aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2023-12-07 15:22:38 +0100
committersonartech <sonartech@sonarsource.com>2023-12-07 20:02:52 +0000
commit63d95c140def63a91bb55caeb50c5f9b9370793f (patch)
tree329a7cbd756df346c52c712c054f2a93e8b6d515 /server
parent664183e0b9b31a5429bbec63f35812f968a62426 (diff)
downloadsonarqube-63d95c140def63a91bb55caeb50c5f9b9370793f.tar.gz
sonarqube-63d95c140def63a91bb55caeb50c5f9b9370793f.zip
SONAR-21089 Improve WebApi2 menu
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx93
2 files changed, 68 insertions, 30 deletions
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 10599c6f00b..5f6fd8ed667 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
@@ -162,8 +162,11 @@ it('should navigate between apis', async () => {
await user.click(ui.queryParameter.byRole('button').getAt(0));
expect(ui.queryParameter.getAt(0)).not.toHaveTextContent('default: available');
- await user.click(ui.apiSidebarItem.getAt(5));
+ await user.click(ui.apiSidebarItem.getAt(4));
expect(await screen.findByText('/api/v3/pet/{petId}')).toBeInTheDocument();
+ expect(
+ await screen.findByText('Updates a pet in the store with form data', { selector: 'h1' }),
+ ).toBeInTheDocument();
expect(ui.queryParameter.getAll()).toHaveLength(2);
expect(ui.pathParameter.getAll()).toHaveLength(1);
expect(ui.requestHeader.query()).not.toBeInTheDocument();
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 6e0c142ac5f..6df26597fed 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
@@ -20,18 +20,22 @@
import classNames from 'classnames';
import {
Badge,
+ BasicSeparator,
Checkbox,
HelperHintIcon,
InputSearch,
Link,
SubnavigationAccordion,
SubnavigationItem,
+ SubnavigationSubheading,
} from 'design-system';
+import { sortBy } from 'lodash';
import { OpenAPIV3 } from 'openapi-types';
-import React, { useMemo, useState } from 'react';
+import React, { Fragment, 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';
interface Api {
@@ -44,6 +48,13 @@ interface Props {
apisList: Api[];
}
+const METHOD_ORDER: Dict<number> = {
+ post: 0,
+ get: 1,
+ patch: 2,
+ delete: 3,
+};
+
export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
const [search, setSearch] = useState('');
const [showInternal, setShowInternal] = useState(false);
@@ -112,38 +123,62 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
key={group}
id={`web-api-${group}`}
>
- {apis.map(({ method, name, info }) => (
- <SubnavigationItem
- active={name === activeApi[0] && method === activeApi[1]}
- key={getApiEndpointKey(name, method)}
- 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>
+ {sortBy(apis, (a) => [a.name, METHOD_ORDER[a.method]]).map(
+ ({ method, name, info }, index, sorted) => {
+ const resourceName = getResourceFromName(name);
+ const previousResourceName =
+ index > 0 ? getResourceFromName(sorted[index - 1].name) : undefined;
+ const isNewResource = resourceName !== previousResourceName;
- {(info['x-internal'] || info.deprecated) && (
- <div className="sw-flex sw-flex-col sw-justify-center sw-gap-2">
- {info['x-internal'] && (
- <Badge variant="new" className="sw-self-center">
- {translate('internal')}
- </Badge>
- )}
- {info.deprecated && (
- <Badge variant="deleted" className="sw-self-center">
- {translate('deprecated')}
+ return (
+ <Fragment key={getApiEndpointKey(name, method)}>
+ {index > 0 && isNewResource && <BasicSeparator />}
+ {(index === 0 || isNewResource) && (
+ <SubnavigationSubheading>{resourceName}</SubnavigationSubheading>
+ )}
+ <SubnavigationItem
+ active={name === activeApi[0] && method === activeApi[1]}
+ 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>
- )}
- </div>
- </SubnavigationItem>
- ))}
+ <div>{info.summary ?? name}</div>
+
+ {(info['x-internal'] || info.deprecated) && (
+ <div className="sw-flex sw-flex-col sw-justify-center sw-gap-2">
+ {info['x-internal'] && (
+ <Badge variant="new" className="sw-self-center">
+ {translate('internal')}
+ </Badge>
+ )}
+ {info.deprecated && (
+ <Badge variant="deleted" className="sw-self-center">
+ {translate('deprecated')}
+ </Badge>
+ )}
+ </div>
+ )}
+ </div>
+ </SubnavigationItem>
+ </Fragment>
+ );
+ },
+ )}
</SubnavigationAccordion>
))}
</>
);
}
+
+function getResourceFromName(name: string) {
+ const parts = name.split('/').slice(2); // remove domain + pre-slash empty string
+
+ if (name.endsWith('}')) {
+ parts.pop(); // remove the resource id
+ }
+
+ return parts.join('/');
+}