]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21057 Support internal parameters
authorViktor Vorona <viktor.vorona@sonarsource.com>
Tue, 2 Jan 2024 13:11:30 +0000 (14:11 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 4 Jan 2024 20:02:47 +0000 (20:02 +0000)
12 files changed:
server/sonar-web/src/main/js/api/mocks/data/web-api.ts
server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx
server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiFilterContext.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiInformation.tsx
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestParameters.tsx
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx
server/sonar-web/src/main/js/apps/web-api-v2/components/RestMethodPill.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/web-api-v2/types.ts
server/sonar-web/src/main/js/apps/web-api-v2/utils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7cf57325fbbeb513e3ab05596a26742a9d77ff23..e6aeb205ecf6f209e2d4c5ae647bcadebc7f2378 100644 (file)
@@ -18,8 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { OpenAPIV3 } from 'openapi-types';
+import { InternalExtension } from '../../../apps/web-api-v2/types';
 
-export const openApiTestData: OpenAPIV3.Document<{ 'x-internal'?: 'true' }> = {
+export const openApiTestData: OpenAPIV3.Document<InternalExtension> = {
   openapi: '3.0.2',
   info: {
     title: 'Swagger Petstore - OpenAPI 3.0',
@@ -397,6 +398,55 @@ export const openApiTestData: OpenAPIV3.Document<{ 'x-internal'?: 'true' }> = {
         },
       },
     },
+    '/test': {
+      get: {
+        summary: 'Test internal query params',
+        description: 'For tests only',
+        parameters: [
+          {
+            name: 'visible',
+            in: 'query',
+            description: 'parameter visible to anyone',
+            schema: { type: 'integer', format: 'int64' },
+          },
+          {
+            name: 'hidden',
+            in: 'query',
+            description: 'parameter is internal',
+            schema: { type: 'integer', format: 'int64' },
+            'x-internal': 'true',
+          } as OpenAPIV3.ParameterObject & InternalExtension,
+        ],
+        responses: {
+          default: {
+            description: 'successful operation',
+          },
+        },
+      },
+      post: {
+        summary: 'Test internal query params',
+        description: 'For tests only',
+        requestBody: {
+          content: {
+            'application/json': {
+              schema: {
+                type: 'object',
+                properties: {
+                  visible: { type: 'string' },
+                  hidden: { type: 'string', 'x-internal': 'true' } as OpenAPIV3.SchemaObject &
+                    InternalExtension,
+                },
+              },
+            },
+          },
+        },
+        responses: {
+          default: {
+            description: 'successful operation',
+          },
+        },
+      },
+    },
     '/user': {
       post: {
         tags: ['user'],
index b217d97e1d47b76c1a0d0aa5e588f4c158e639d2..df1ff1ff3dfb239a318509a3ccb2a02a15bd3cdb 100644 (file)
 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>
   );
 }
 
index 708698d5f6c9f3efcfca6c8c74ec7e47ca1d1765..f0ff88411960e73978d0977b83b345105fbe7088 100644 (file)
@@ -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 (file)
index 0000000..5c669dc
--- /dev/null
@@ -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;
index f1446b8da2f5b739e9f21c80fef2f518c87e9c2b..d6da453d68eea1af810b2ded752977d0e488b755 100644 (file)
  * 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')}
index 113ddc8999f83197dd10194b5e2f61002d9662a2..e0b3657da16b134b6738479f8996774c0ad635c1 100644 (file)
 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>
index b1b78e9cbba901624d0827e447de4e01579a2480..42bc04e3ce80b5640c17bea74e965ed31889f559 100644 (file)
 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>
   );
 }
index 86fca9b65ab6f17b208d36e86b62ebf92efd8853..e8ef17258c182448b141db73474b8e0cca999aad 100644 (file)
@@ -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 (file)
index 0000000..8387e0a
--- /dev/null
@@ -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>
+  );
+}
index 22e1e34dd7c4c36264c12649c90f5140612fa099..30d2f6cb2b746a527ed58ee2a654417e3028bde3 100644 (file)
@@ -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';
+}
index da52b8c33c81c499aa4d1ab09c9d083ff93cb631..7e8826d05f9330ee8394017b83d118fc32edc331 100644 (file)
@@ -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':
index f2f4cbb63d00c65489d0a17e90cbe38e95c1efb1..5864c0816f19db43f90bc7cbe7f30493caeca8e2 100644 (file)
@@ -4018,10 +4018,12 @@ overview.activity.variations.first_analysis=First analysis:
 #------------------------------------------------------------------------------
 api_documentation.deprecation_tooltip=An API deprecated in version X.Y will be dropped in version (X+1).0. Example: an API deprecated in 4.1 is supported in 4.X (4.2, 4.3, etc.) and will be dropped in version 5.0.
 api_documentation.internal_tooltip=Use at your own risk; internal services are subject to change or removal without notice.
+api_documentation.internal_tooltip_v2=Use at your own risk. Shows/hides the internal endpoints, parameters, and other details used for internal services. These are subject to change or removal without notice.
 api_documentation.page=Web API
 api_documentation.page.v2=Web API v2
 api_documentation.show_deprecated=Show Deprecated API
 api_documentation.show_internal=Show Internal API
+api_documentation.show_internal_v2=Show Internal
 api_documentation.possible_values=Possible values
 api_documentation.default_values=Default value
 api_documentation.example_values=Example value