diff options
author | Viktor Vorona <viktor.vorona@sonarsource.com> | 2024-01-02 14:11:30 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-01-04 20:02:47 +0000 |
commit | 413d83d819d583309e6185605f8222d64e29d2f0 (patch) | |
tree | 677b597b29e4bf19e0ab3638b5cd96f64e9185e2 /server/sonar-web/src/main/js/apps/web-api-v2 | |
parent | b6f0a55c06cabbf1e9f0a1ac6d9dc5840a670ad9 (diff) | |
download | sonarqube-413d83d819d583309e6185605f8222d64e29d2f0.tar.gz sonarqube-413d83d819d583309e6185605f8222d64e29d2f0.zip |
SONAR-21057 Support internal parameters
Diffstat (limited to 'server/sonar-web/src/main/js/apps/web-api-v2')
10 files changed, 369 insertions, 218 deletions
diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx index b217d97e1d4..df1ff1ff3df 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx @@ -20,16 +20,18 @@ import styled from '@emotion/styled'; import { LargeCenteredLayout, PageContentFontWrapper, Spinner, Title } from 'design-system'; import { omit } from 'lodash'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { useLocation } from 'react-router-dom'; import { translate } from '../../helpers/l10n'; import { useOpenAPI } from '../../queries/web-api'; +import ApiFilterContext from './components/ApiFilterContext'; import ApiInformation from './components/ApiInformation'; import ApiSidebar from './components/ApiSidebar'; import { URL_DIVIDER, dereferenceSchema } from './utils'; export default function WebApiApp() { + const [showInternal, setShowInternal] = useState(false); const { data, isLoading } = useOpenAPI(); const location = useLocation(); const activeApi = location.hash.replace('#', '').split(URL_DIVIDER); @@ -53,51 +55,61 @@ export default function WebApiApp() { activeApi.length > 1 && apis.find((api) => api.name === activeApi[0] && api.method === activeApi[1]); + const contextValue = useMemo( + () => ({ + showInternal, + setShowInternal, + }), + [showInternal], + ); + return ( - <LargeCenteredLayout> - <PageContentFontWrapper className="sw-body-sm"> - <Helmet defer={false} title={translate('api_documentation.page')} /> - <Spinner loading={isLoading}> - {data && ( - <div className="sw-w-full sw-flex"> - <NavContainer aria-label={translate('api_documentation.page')} className="sw--mx-2"> - <div className="sw-w-[300px] lg:sw-w-[390px] sw-mx-2"> - <ApiSidebar - docInfo={data.info} - apisList={apis.map(({ name, method, info }) => ({ - method, - name, - info, - }))} - /> - </div> - </NavContainer> - <main - className="sw-relative sw-ml-12 sw-flex-1 sw-overflow-y-auto sw-py-6" - style={{ height: 'calc(100vh - 160px)' }} - > - <Spinner loading={isLoading}> - {!activeData && ( - <> - <Title>{translate('about')}</Title> - <p>{data.info.description}</p> - </> - )} - {data && activeData && ( - <ApiInformation - apiUrl={data.servers?.[0]?.url ?? ''} - name={activeData.name} - data={activeData.info} - method={activeData.method} + <ApiFilterContext.Provider value={contextValue}> + <LargeCenteredLayout> + <PageContentFontWrapper className="sw-body-sm"> + <Helmet defer={false} title={translate('api_documentation.page')} /> + <Spinner loading={isLoading}> + {data && ( + <div className="sw-w-full sw-flex"> + <NavContainer aria-label={translate('api_documentation.page')} className="sw--mx-2"> + <div className="sw-w-[300px] lg:sw-w-[390px] sw-mx-2"> + <ApiSidebar + docInfo={data.info} + apisList={apis.map(({ name, method, info }) => ({ + method, + name, + info, + }))} /> - )} - </Spinner> - </main> - </div> - )} - </Spinner> - </PageContentFontWrapper> - </LargeCenteredLayout> + </div> + </NavContainer> + <main + className="sw-relative sw-ml-12 sw-flex-1 sw-overflow-y-auto sw-py-6" + style={{ height: 'calc(100vh - 160px)' }} + > + <Spinner loading={isLoading}> + {!activeData && ( + <> + <Title>{translate('about')}</Title> + <p>{data.info.description}</p> + </> + )} + {data && activeData && ( + <ApiInformation + apiUrl={data.servers?.[0]?.url ?? ''} + name={activeData.name} + data={activeData.info} + method={activeData.method} + /> + )} + </Spinner> + </main> + </div> + )} + </Spinner> + </PageContentFontWrapper> + </LargeCenteredLayout> + </ApiFilterContext.Provider> ); } diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx index 708698d5f6c..f0ff8841196 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx @@ -31,10 +31,13 @@ const ui = { search: byRole('searchbox'), title: byRole('link', { name: 'Swagger Petstore - OpenAPI 3.0 1.0.17' }), searchClear: byRole('button', { name: 'clear' }), - showInternal: byRole('checkbox', { name: 'api_documentation.show_internal' }), + showInternal: byRole('checkbox', { name: 'api_documentation.show_internal_v2' }), apiScopePet: byRole('button', { name: 'pet' }), apiScopeStore: byRole('button', { name: 'store' }), apiScopeUser: byRole('button', { name: 'user' }), + apiScopeTest: byRole('button', { name: 'test' }), + publicButton: byRole('button', { name: /visible/ }), + internalButton: byRole('button', { name: /hidden/ }), apiSidebarItem: byTestId('js-subnavigation-item'), requestBody: byText('api_documentation.v2.request_subheader.request_body'), queryParameter: byRole('list', { name: 'api_documentation.v2.request_subheader.query' }).byRole( @@ -86,7 +89,7 @@ it('should search apis', async () => { expect(ui.apiSidebarItem.getAll().length).toBeGreaterThan(3); }); -it('should show internal', async () => { +it('should show internal endpoints', async () => { const user = userEvent.setup(); renderWebApiApp(); expect(await ui.apiScopeStore.find()).toBeInTheDocument(); @@ -99,9 +102,41 @@ it('should show internal', async () => { .getAll() .find((el) => el.textContent?.includes('internal')); expect(internalItem).toBeInTheDocument(); - await user.click(internalItem!); + await user.click(internalItem as HTMLElement); - expect(await screen.findByText('/api/v3/store/inventory')).toHaveTextContent(/internal/); + expect(await byRole('heading', { name: /\/api\/v3\/store\/inventory/ }).find()).toHaveTextContent( + /internal/, + ); +}); + +it('should show internal parameters', async () => { + const user = userEvent.setup(); + renderWebApiApp(); + expect(await ui.apiScopeTest.find()).toBeInTheDocument(); + await user.click(ui.apiScopeTest.get()); + expect(ui.apiSidebarItem.getAll()).toHaveLength(2); + + await user.click( + ui.apiSidebarItem.getAll().find((el) => el.textContent?.includes('GET')) as HTMLElement, + ); + expect(await ui.publicButton.find()).toBeInTheDocument(); + expect(ui.internalButton.query()).not.toBeInTheDocument(); + await user.click(ui.showInternal.get()); + expect(ui.publicButton.get()).toBeInTheDocument(); + expect(ui.publicButton.get()).not.toHaveTextContent('internal'); + expect(ui.internalButton.get()).toBeInTheDocument(); + expect(ui.internalButton.get()).toHaveTextContent('internal'); + + await user.click( + ui.apiSidebarItem.getAll().find((el) => el.textContent?.includes('POST')) as HTMLElement, + ); + expect(ui.publicButton.get()).toBeInTheDocument(); + expect(ui.publicButton.get()).not.toHaveTextContent('internal'); + expect(ui.internalButton.get()).toBeInTheDocument(); + expect(ui.internalButton.get()).toHaveTextContent('internal'); + await user.click(ui.showInternal.get()); + expect(await ui.publicButton.find()).toBeInTheDocument(); + expect(ui.internalButton.query()).not.toBeInTheDocument(); }); it('should navigate between apis', async () => { diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiFilterContext.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiFilterContext.tsx new file mode 100644 index 00000000000..5c669dc0213 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiFilterContext.tsx @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { Dispatch, SetStateAction, createContext } from 'react'; + +const ApiFilterContext = createContext<{ + showInternal: boolean; + setShowInternal: Dispatch<SetStateAction<boolean>>; +}>({ + showInternal: false, + setShowInternal: () => {}, +}); + +export default ApiFilterContext; diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiInformation.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiInformation.tsx index f1446b8da2f..d6da453d68e 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiInformation.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiInformation.tsx @@ -17,18 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; import { Badge, SubHeading, Title } from 'design-system'; import { OpenAPIV3 } from 'openapi-types'; import React from 'react'; import { translate } from '../../../helpers/l10n'; -import { ExcludeReferences } from '../types'; -import { getApiEndpointKey, getMethodClassName } from '../utils'; +import { ExcludeReferences, InternalExtension } from '../types'; +import { getApiEndpointKey } from '../utils'; import ApiParameters from './ApiParameters'; import ApiResponses from './ApiResponses'; +import RestMethodPill from './RestMethodPill'; interface Props { - data: ExcludeReferences<OpenAPIV3.OperationObject<{ 'x-internal'?: 'true' }>>; + data: ExcludeReferences<OpenAPIV3.OperationObject<InternalExtension>>; apiUrl: string; name: string; method: string; @@ -39,10 +39,8 @@ export default function ApiInformation({ name, data, method, apiUrl }: Readonly< <> {data.summary && <Title>{data.summary}</Title>} <SubHeading> - <Badge className={classNames('sw-align-middle sw-mr-4', getMethodClassName(method))}> - {method} - </Badge> - {apiUrl.replace(/.*(?=\/api)/, '') + name} + <RestMethodPill method={method} /> + <span className="sw-ml-4">{apiUrl.replace(/.*(?=\/api)/, '') + name}</span> {data['x-internal'] && ( <Badge variant="new" className="sw-ml-3"> {translate('internal')} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx index 113ddc8999f..e0b3657da16 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx @@ -20,11 +20,12 @@ import { Accordion, Badge, SubHeading, SubTitle, TextMuted } from 'design-system'; import { groupBy } from 'lodash'; import { OpenAPIV3 } from 'openapi-types'; -import React from 'react'; +import React, { useContext } from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; -import { ExcludeReferences } from '../types'; +import { ExcludeReferences, InternalExtension } from '../types'; import { mapOpenAPISchema } from '../utils'; +import ApiFilterContext from './ApiFilterContext'; import ApiRequestBodyParameters from './ApiRequestParameters'; import ApiResponseSchema from './ApiResponseSchema'; @@ -32,8 +33,9 @@ interface Props { data: ExcludeReferences<OpenAPIV3.OperationObject>; } -export default function ApiParameters({ data }: Props) { +export default function ApiParameters({ data }: Readonly<Props>) { const [openParameters, setOpenParameters] = React.useState<string[]>([]); + const { showInternal } = useContext(ApiFilterContext); const toggleParameter = (name: string) => { if (openParameters.includes(name)) { @@ -54,85 +56,97 @@ export default function ApiParameters({ data }: Props) { return ( <> <SubTitle>{translate('api_documentation.v2.parameter_header')}</SubTitle> - {Object.entries(groupBy(data.parameters, (p) => p.in)).map(([group, parameters]) => ( - <div key={group}> - <SubHeading id={`api-parameters-${group}`}> - {translate(`api_documentation.v2.request_subheader.${group}`)} - </SubHeading> - <ul aria-labelledby={`api-parameters-${group}`}> - {parameters.map((parameter) => { - return ( - <Accordion - className="sw-mt-2 sw-mb-4" - key={parameter.name} - header={ - <div> - {parameter.name}{' '} - {parameter.schema && ( + {Object.entries(groupBy(data.parameters, (p) => p.in)).map( + ([group, parameters]: [ + string, + ExcludeReferences<Array<OpenAPIV3.ParameterObject & InternalExtension>>, + ]) => ( + <div key={group}> + <SubHeading id={`api-parameters-${group}`}> + {translate(`api_documentation.v2.request_subheader.${group}`)} + </SubHeading> + <ul aria-labelledby={`api-parameters-${group}`}> + {parameters + .filter((parameter) => showInternal || !parameter['x-internal']) + .map((parameter) => { + return ( + <Accordion + className="sw-mt-2 sw-mb-4" + key={parameter.name} + header={ + <div> + {parameter.name}{' '} + {parameter.schema && ( + <TextMuted + className="sw-inline sw-ml-2" + text={getSchemaType(parameter.schema)} + /> + )} + {parameter.required && ( + <Badge className="sw-ml-2">{translate('required')}</Badge> + )} + {parameter.deprecated && ( + <Badge variant="deleted" className="sw-ml-2"> + {translate('deprecated')} + </Badge> + )} + {parameter['x-internal'] && ( + <Badge variant="new" className="sw-ml-2"> + {translate('internal')} + </Badge> + )} + </div> + } + data={parameter.name} + onClick={toggleParameter} + open={openParameters.includes(parameter.name)} + > + <div>{parameter.description}</div> + {parameter.schema?.enum && ( + <div className="sw-mt-2"> + <FormattedMessage + id="api_documentation.v2.enum_description" + values={{ + values: ( + <div className="sw-body-sm-highlight"> + {parameter.schema.enum.join(', ')} + </div> + ), + }} + /> + </div> + )} + {parameter.schema?.maximum && ( <TextMuted - className="sw-inline sw-ml-2" - text={getSchemaType(parameter.schema)} + className="sw-mt-2 sw-block" + text={`${translate('max')}: ${parameter.schema?.maximum}`} /> )} - {parameter.required && ( - <Badge className="sw-ml-2">{translate('required')}</Badge> + {typeof parameter.schema?.minimum === 'number' && ( + <TextMuted + className="sw-mt-2 sw-block" + text={`${translate('min')}: ${parameter.schema?.minimum}`} + /> )} - {parameter.deprecated && ( - <Badge variant="deleted" className="sw-ml-2"> - {translate('deprecated')} - </Badge> + {parameter.example !== undefined && ( + <TextMuted + className="sw-mt-2 sw-block" + text={`${translate('example')}: ${parameter.example}`} + /> )} - </div> - } - data={parameter.name} - onClick={toggleParameter} - open={openParameters.includes(parameter.name)} - > - <div>{parameter.description}</div> - {parameter.schema?.enum && ( - <div className="sw-mt-2"> - <FormattedMessage - id="api_documentation.v2.enum_description" - values={{ - values: ( - <div className="sw-body-sm-highlight"> - {parameter.schema.enum.join(', ')} - </div> - ), - }} - /> - </div> - )} - {parameter.schema?.maximum && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('max')}: ${parameter.schema?.maximum}`} - /> - )} - {typeof parameter.schema?.minimum === 'number' && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('min')}: ${parameter.schema?.minimum}`} - /> - )} - {parameter.example !== undefined && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('example')}: ${parameter.example}`} - /> - )} - {parameter.schema?.default !== undefined && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('default')}: ${parameter.schema?.default}`} - /> - )} - </Accordion> - ); - })} - </ul> - </div> - ))} + {parameter.schema?.default !== undefined && ( + <TextMuted + className="sw-mt-2 sw-block" + text={`${translate('default')}: ${parameter.schema?.default}`} + /> + )} + </Accordion> + ); + })} + </ul> + </div> + ), + )} {!requestBody && !data.parameters?.length && <TextMuted text={translate('no_data')} />} {requestBody && ( <div> diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestParameters.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestParameters.tsx index b1b78e9cbba..42bc04e3ce8 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestParameters.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestParameters.tsx @@ -20,17 +20,19 @@ import { Accordion, Badge, TextMuted } from 'design-system'; import { isEmpty } from 'lodash'; import { OpenAPIV3 } from 'openapi-types'; -import React from 'react'; +import React, { useContext } from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; -import { ExcludeReferences } from '../types'; +import { ExcludeReferences, InternalExtension } from '../types'; +import ApiFilterContext from './ApiFilterContext'; interface Props { - content?: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>; + content?: ExcludeReferences<OpenAPIV3.ResponseObject>['content']; } export default function ApiRequestBodyParameters({ content }: Readonly<Props>) { const [openParameters, setOpenParameters] = React.useState<string[]>([]); + const { showInternal } = useContext(ApiFilterContext); const toggleParameter = (parameter: string) => { if (openParameters.includes(parameter)) { @@ -63,60 +65,72 @@ export default function ApiRequestBodyParameters({ content }: Readonly<Props>) { return ( <ul aria-labelledby="api_documentation.v2.request_subheader.request_body"> - {orderedKeys.map((key) => { - return ( - <Accordion - className="sw-mt-2 sw-mb-4" - key={key} - header={ - <div> - {key}{' '} - {schema.required?.includes(key) && ( - <Badge className="sw-ml-2">{translate('required')}</Badge> - )} - {parameters[key].deprecated && ( - <Badge variant="deleted" className="sw-ml-2"> - {translate('deprecated')} - </Badge> - )} - </div> - } - data={key} - onClick={() => toggleParameter(key)} - open={openParameters.includes(key)} - > - <div>{parameters[key].description}</div> - {parameters[key].enum && ( - <div className="sw-mt-2"> - <FormattedMessage - id="api_documentation.v2.enum_description" - values={{ - values: <i>{parameters[key].enum?.join(', ')}</i>, - }} + {orderedKeys + .filter((key) => showInternal || !(parameters[key] as InternalExtension)['x-internal']) + .map((key) => { + return ( + <Accordion + className="sw-mt-2 sw-mb-4" + key={key} + header={ + <div> + {key}{' '} + {schema.required?.includes(key) && ( + <Badge className="sw-ml-2">{translate('required')}</Badge> + )} + {parameters[key].deprecated && ( + <Badge variant="deleted" className="sw-ml-2"> + {translate('deprecated')} + </Badge> + )} + {parameters[key].deprecated && ( + <Badge variant="deleted" className="sw-ml-2"> + {translate('deprecated')} + </Badge> + )} + {(parameters[key] as InternalExtension)['x-internal'] && ( + <Badge variant="new" className="sw-ml-2"> + {translate('internal')} + </Badge> + )} + </div> + } + data={key} + onClick={() => toggleParameter(key)} + open={openParameters.includes(key)} + > + <div>{parameters[key].description}</div> + {parameters[key].enum && ( + <div className="sw-mt-2"> + <FormattedMessage + id="api_documentation.v2.enum_description" + values={{ + values: <i>{parameters[key].enum?.join(', ')}</i>, + }} + /> + </div> + )} + {parameters[key].maxLength && ( + <TextMuted + className="sw-mt-2 sw-block" + text={`${translate('max')}: ${parameters[key].maxLength}`} /> - </div> - )} - {parameters[key].maxLength && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('max')}: ${parameters[key].maxLength}`} - /> - )} - {typeof parameters[key].minLength === 'number' && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('min')}: ${parameters[key].minLength}`} - /> - )} - {parameters[key].default !== undefined && ( - <TextMuted - className="sw-mt-2 sw-block" - text={`${translate('default')}: ${parameters[key].default}`} - /> - )} - </Accordion> - ); - })} + )} + {typeof parameters[key].minLength === 'number' && ( + <TextMuted + className="sw-mt-2 sw-block" + text={`${translate('min')}: ${parameters[key].minLength}`} + /> + )} + {parameters[key].default !== undefined && ( + <TextMuted + className="sw-mt-2 sw-block" + text={`${translate('default')}: ${parameters[key].default}`} + /> + )} + </Accordion> + ); + })} </ul> ); } diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx index 86fca9b65ab..e8ef17258c1 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; import { Badge, BasicSeparator, @@ -31,17 +30,20 @@ import { } from 'design-system'; import { sortBy } from 'lodash'; import { OpenAPIV3 } from 'openapi-types'; -import React, { Fragment, useMemo, useState } from 'react'; +import React, { Fragment, useContext, useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import { Dict } from '../../../types/types'; -import { URL_DIVIDER, getApiEndpointKey, getMethodClassName } from '../utils'; +import { InternalExtension } from '../types'; +import { URL_DIVIDER, getApiEndpointKey } from '../utils'; +import ApiFilterContext from './ApiFilterContext'; +import RestMethodPill from './RestMethodPill'; interface Api { name: string; method: string; - info: OpenAPIV3.OperationObject<{ 'x-internal'?: 'true' }>; + info: OpenAPIV3.OperationObject<InternalExtension>; } interface Props { docInfo: OpenAPIV3.InfoObject; @@ -57,9 +59,9 @@ const METHOD_ORDER: Dict<number> = { export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) { const [search, setSearch] = useState(''); - const [showInternal, setShowInternal] = useState(false); const navigate = useNavigate(); const location = useLocation(); + const { showInternal, setShowInternal } = useContext(ApiFilterContext); const activeApi = location.hash.replace('#', '').split(URL_DIVIDER); const handleApiClick = (value: string) => { @@ -106,9 +108,12 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) { <div className="sw-mt-4 sw-flex sw-items-center"> <Checkbox checked={showInternal} onCheck={() => setShowInternal((prev) => !prev)}> - <span className="sw-ml-2">{translate('api_documentation.show_internal')}</span> + <span className="sw-ml-2">{translate('api_documentation.show_internal_v2')}</span> </Checkbox> - <HelpTooltip className="sw-ml-2" overlay={translate('api_documentation.internal_tooltip')}> + <HelpTooltip + className="sw-ml-2" + overlay={translate('api_documentation.internal_tooltip_v2')} + > <HelperHintIcon aria-label="help-tooltip" /> </HelpTooltip> </div> @@ -141,11 +146,11 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) { onClick={handleApiClick} value={getApiEndpointKey(name, method)} > - <div className="sw-flex sw-gap-2"> - <Badge className={classNames('sw-self-center', getMethodClassName(method))}> - {method.toUpperCase()} - </Badge> - <div>{info.summary ?? name}</div> + <div className="sw-flex sw-gap-2 sw-w-full sw-justify-between"> + <div className="sw-flex sw-gap-2"> + <RestMethodPill method={method} /> + <div>{info.summary ?? name}</div> + </div> {(info['x-internal'] || info.deprecated) && ( <div className="sw-flex sw-flex-col sw-justify-center sw-gap-2"> diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/RestMethodPill.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/RestMethodPill.tsx new file mode 100644 index 00000000000..8387e0adf73 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/RestMethodPill.tsx @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import classNames from 'classnames'; +import { Badge } from 'design-system'; +import React from 'react'; + +interface Props { + method: string; +} + +export default function RestMethodPill({ method }: Readonly<Props>) { + const getMethodClassName = (): string => { + switch (method.toLowerCase()) { + case 'get': + return 'sw-bg-green-200'; + case 'delete': + return 'sw-bg-red-200'; + case 'post': + return 'sw-bg-blue-200'; + case 'put': + return 'sw-bg-purple-200'; + case 'patch': + return 'sw-bg-yellow-200'; + default: + return 'sw-bg-gray-200'; + } + }; + + return ( + <Badge + className={classNames( + 'sw-self-center sw-align-middle sw-min-w-[50px] sw-text-center', + getMethodClassName(), + )} + > + {method.toUpperCase()} + </Badge> + ); +} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/types.ts b/server/sonar-web/src/main/js/apps/web-api-v2/types.ts index 22e1e34dd7c..30d2f6cb2b7 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/types.ts +++ b/server/sonar-web/src/main/js/apps/web-api-v2/types.ts @@ -34,3 +34,7 @@ export type DereferenceRecursive<T> = T extends object [K in keyof T]: DereferenceRecursive<T[K]>; } : T; + +export interface InternalExtension { + 'x-internal'?: 'true'; +} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts b/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts index da52b8c33c8..7e8826d05f9 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts +++ b/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts @@ -72,23 +72,6 @@ export const dereferenceSchema = ( return dereferenceRecursive(document) as ExcludeReferences<OpenAPIV3.Document>; }; -export const getMethodClassName = (method: string): string => { - switch (method.toLowerCase()) { - case 'get': - return 'sw-bg-green-200'; - case 'delete': - return 'sw-bg-red-200'; - case 'post': - return 'sw-bg-blue-200'; - case 'put': - return 'sw-bg-purple-200'; - case 'patch': - return 'sw-bg-yellow-200'; - default: - return 'sw-bg-gray-200'; - } -}; - export const getResponseCodeClassName = (code: string): string => { switch (code[0]) { case '1': |