aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2024-03-28 16:53:12 +0100
committersonartech <sonartech@sonarsource.com>2024-04-02 20:02:42 +0000
commitb94103d175321a9b2a447295b42efce0d3f27a67 (patch)
tree628a6bc7904e29265c05f58e6eeea43e51d93c67 /server/sonar-web/src/main/js/apps
parent472bb6484765a2b5c6e36b2c77f84d071690a5fa (diff)
downloadsonarqube-b94103d175321a9b2a447295b42efce0d3f27a67.tar.gz
sonarqube-b94103d175321a9b2a447295b42efce0d3f27a67.zip
SONAR-21826 Display content-type in APIv2 documentation
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts310
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/utils.ts29
6 files changed, 273 insertions, 147 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 df1ff1ff3df..c041e68f028 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
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
-import { LargeCenteredLayout, PageContentFontWrapper, Spinner, Title } from 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
+import { LargeCenteredLayout, PageContentFontWrapper, Title } from 'design-system';
import { omit } from 'lodash';
import React, { useMemo, useState } from 'react';
import { Helmet } from 'react-helmet-async';
@@ -68,7 +69,7 @@ export default function WebApiApp() {
<LargeCenteredLayout>
<PageContentFontWrapper className="sw-body-sm">
<Helmet defer={false} title={translate('api_documentation.page')} />
- <Spinner loading={isLoading}>
+ <Spinner isLoading={isLoading}>
{data && (
<div className="sw-w-full sw-flex">
<NavContainer aria-label={translate('api_documentation.page')} className="sw--mx-2">
@@ -87,7 +88,7 @@ export default function WebApiApp() {
className="sw-relative sw-ml-12 sw-flex-1 sw-overflow-y-auto sw-py-6"
style={{ height: 'calc(100vh - 160px)' }}
>
- <Spinner loading={isLoading}>
+ <Spinner isLoading={isLoading}>
{!activeData && (
<>
<Title>{translate('about')}</Title>
diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts
index b7009f9a7fe..cf3c3618b17 100644
--- a/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts
@@ -17,11 +17,89 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { dereferenceSchema, mapOpenAPISchema } from '../utils';
+import { dereferenceSchema, extractSchemaAndMediaType, mapOpenAPISchema } from '../utils';
-it('should dereference schema', () => {
- expect(
- dereferenceSchema({
+describe('dereferenceSchema', () => {
+ it('should dereference schema', () => {
+ expect(
+ dereferenceSchema({
+ openapi: '3.0.1',
+ info: {
+ title: 'SonarQube Web API',
+ version: '1.0.0 beta',
+ },
+ paths: {
+ '/test': {
+ delete: {
+ responses: {
+ '200': {
+ description: 'Internal Server Error',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Test',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ '/test/{first}': {
+ get: {
+ parameters: [
+ {
+ name: 'first',
+ in: 'path',
+ description: '1',
+ schema: {
+ $ref: '#/components/schemas/NestedTest',
+ },
+ },
+ {
+ name: 'second',
+ in: 'query',
+ description: '2',
+ schema: {
+ type: 'string',
+ },
+ },
+ ],
+ responses: {
+ '200': {
+ description: 'Internal Server Error',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/NestedTest',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ components: {
+ schemas: {
+ NestedTest: {
+ type: 'object',
+ properties: {
+ test: {
+ type: 'array',
+ items: {
+ $ref: '#/components/schemas/Test',
+ },
+ },
+ },
+ },
+ Test: {
+ type: 'string',
+ },
+ },
+ },
+ }),
+ ).toStrictEqual({
openapi: '3.0.1',
info: {
title: 'SonarQube Web API',
@@ -36,7 +114,7 @@ it('should dereference schema', () => {
content: {
'application/json': {
schema: {
- $ref: '#/components/schemas/Test',
+ type: 'string',
},
},
},
@@ -52,7 +130,15 @@ it('should dereference schema', () => {
in: 'path',
description: '1',
schema: {
- $ref: '#/components/schemas/NestedTest',
+ type: 'object',
+ properties: {
+ test: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
},
},
{
@@ -70,7 +156,15 @@ it('should dereference schema', () => {
content: {
'application/json': {
schema: {
- $ref: '#/components/schemas/NestedTest',
+ type: 'object',
+ properties: {
+ test: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
},
},
},
@@ -87,7 +181,7 @@ it('should dereference schema', () => {
test: {
type: 'array',
items: {
- $ref: '#/components/schemas/Test',
+ type: 'string',
},
},
},
@@ -97,143 +191,95 @@ it('should dereference schema', () => {
},
},
},
- }),
- ).toStrictEqual({
- openapi: '3.0.1',
- info: {
- title: 'SonarQube Web API',
- version: '1.0.0 beta',
- },
- paths: {
- '/test': {
- delete: {
- responses: {
- '200': {
- description: 'Internal Server Error',
- content: {
- 'application/json': {
- schema: {
- type: 'string',
- },
- },
- },
- },
+ });
+ });
+});
+
+describe('mapOpenAPISchema', () => {
+ it('should map open api response schema', () => {
+ expect(
+ mapOpenAPISchema({
+ type: 'object',
+ properties: {
+ str: {
+ type: 'string',
},
- },
- },
- '/test/{first}': {
- get: {
- parameters: [
- {
- name: 'first',
- in: 'path',
- description: '1',
- schema: {
- type: 'object',
- properties: {
- test: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- },
- },
- },
- {
- name: 'second',
- in: 'query',
- description: '2',
- schema: {
- type: 'string',
- },
- },
- ],
- responses: {
- '200': {
- description: 'Internal Server Error',
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- test: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- },
- },
- },
- },
- },
+ int: {
+ type: 'integer',
+ format: 'int32',
},
- },
- },
- },
- components: {
- schemas: {
- NestedTest: {
- type: 'object',
- properties: {
- test: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
+ num: {
+ type: 'number',
+ format: 'double',
+ },
+ bool: {
+ type: 'boolean',
},
},
- Test: {
+ }),
+ ).toStrictEqual({
+ str: 'string',
+ int: 'integer (int32)',
+ num: 'number (double)',
+ bool: 'boolean',
+ });
+
+ expect(
+ mapOpenAPISchema({
+ type: 'array',
+ items: {
type: 'string',
},
- },
- },
+ }),
+ ).toStrictEqual(['string']);
+
+ expect(
+ mapOpenAPISchema({
+ type: 'string',
+ enum: ['GREEN', 'YELLOW', 'RED'],
+ }),
+ ).toStrictEqual('Enum (string): GREEN, YELLOW, RED');
});
});
-it('should map open api response schema', () => {
- expect(
- mapOpenAPISchema({
- type: 'object',
- properties: {
- str: {
- type: 'string',
- },
- int: {
- type: 'integer',
- format: 'int32',
- },
- num: {
- type: 'number',
- format: 'double',
+describe('extractSchemaAndMediaType', () => {
+ it('should extract the schema', () => {
+ const result = extractSchemaAndMediaType({
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', description: 'username' },
+ age: { type: 'number', description: 'age', minimum: 0, maximum: 130 },
+ },
},
- bool: {
- type: 'boolean',
+ },
+ 'application/merge-patch+json': {
+ schema: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', description: 'username' },
+ age: { type: 'number', description: 'age', minimum: 0, maximum: 130 },
+ },
},
},
- }),
- ).toStrictEqual({
- str: 'string',
- int: 'integer (int32)',
- num: 'number (double)',
- bool: 'boolean',
+ });
+
+ expect(result).toHaveLength(2);
});
- expect(
- mapOpenAPISchema({
- type: 'array',
- items: {
- type: 'string',
- },
- }),
- ).toStrictEqual(['string']);
+ it('should handle missing schema', () => {
+ const result = extractSchemaAndMediaType({
+ 'application/json': {},
+ 'application/merge-patch+json': {},
+ });
+
+ expect(result).toHaveLength(0);
+ });
- expect(
- mapOpenAPISchema({
- type: 'string',
- enum: ['GREEN', 'YELLOW', 'RED'],
- }),
- ).toStrictEqual('Enum (string): GREEN, YELLOW, RED');
+ it('should handle no content', () => {
+ const result = extractSchemaAndMediaType();
+
+ expect(result).toHaveLength(0);
+ });
});
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 e0b3657da16..1e79da3a816 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
@@ -27,7 +27,7 @@ import { ExcludeReferences, InternalExtension } from '../types';
import { mapOpenAPISchema } from '../utils';
import ApiFilterContext from './ApiFilterContext';
import ApiRequestBodyParameters from './ApiRequestParameters';
-import ApiResponseSchema from './ApiResponseSchema';
+import ApiRequestSchema from './ApiRequestSchema';
interface Props {
data: ExcludeReferences<OpenAPIV3.OperationObject>;
@@ -153,7 +153,7 @@ export default function ApiParameters({ data }: Readonly<Props>) {
<SubHeading id="api_documentation.v2.request_subheader.request_body">
{translate('api_documentation.v2.request_subheader.request_body')}
</SubHeading>
- <ApiResponseSchema content={requestBody} />
+ <ApiRequestSchema content={requestBody} />
<ApiRequestBodyParameters content={requestBody} />
</div>
)}
diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx
new file mode 100644
index 00000000000..b7a11e01988
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { Card, CodeSnippet, TextMuted } from 'design-system';
+import { OpenAPIV3 } from 'openapi-types';
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { ExcludeReferences } from '../types';
+import { extractSchemaAndMediaType } from '../utils';
+
+interface Props {
+ content: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>;
+}
+
+export default function ApiRequestSchema(props: Readonly<Props>) {
+ const { content, ...other } = props;
+
+ const results = extractSchemaAndMediaType(content);
+
+ if (results.length === 0) {
+ return <TextMuted text={translate('no_data')} />;
+ }
+
+ return results.map(({ requestMediaType, schema }) => (
+ <Card key={requestMediaType}>
+ <div>
+ <span>{translate('api_documentation.v2.request_subheader.request_content_type')}</span>
+ <CodeSnippet snippet={requestMediaType} isOneLine noCopy />
+ </div>
+ <CodeSnippet language="json" className="sw-p-6" snippet={schema} wrap="words" {...other} />
+ </Card>
+ ));
+}
diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx
index 2fca7f158c3..19cce2011ba 100644
--- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx
+++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx
@@ -19,31 +19,32 @@
*/
import { CodeSnippet, TextMuted } from 'design-system';
import { OpenAPIV3 } from 'openapi-types';
-import React, { HtmlHTMLAttributes } from 'react';
+import React from 'react';
import { translate } from '../../../helpers/l10n';
import { ExcludeReferences } from '../types';
-import { mapOpenAPISchema } from '../utils';
+import { extractSchemaAndMediaType } from '../utils';
-interface Props extends Omit<HtmlHTMLAttributes<HTMLDivElement>, 'content'> {
+interface Props {
content?: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>;
}
export default function ApiResponseSchema(props: Readonly<Props>) {
const { content, ...other } = props;
- const schema =
- content &&
- (content['application/json']?.schema || content['application/merge-patch+json']?.schema);
- if (!schema) {
+
+ const results = extractSchemaAndMediaType(content);
+
+ if (results.length === 0) {
return <TextMuted text={translate('no_data')} />;
}
- return (
+ return results.map(({ requestMediaType, schema }) => (
<CodeSnippet
+ key={requestMediaType}
language="json"
className="sw-p-6"
- snippet={JSON.stringify(mapOpenAPISchema(schema), null, 2)}
+ snippet={schema}
wrap="words"
{...other}
/>
- );
+ ));
}
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 7e8826d05f9..fd139a638a6 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
@@ -19,6 +19,7 @@
*/
import { mapValues } from 'lodash';
import { OpenAPIV3 } from 'openapi-types';
+import { isDefined } from '../../helpers/types';
import { DereferenceRecursive, ExcludeReferences } from './types';
export const URL_DIVIDER = '--';
@@ -89,3 +90,31 @@ export const getResponseCodeClassName = (code: string): string => {
};
export const getApiEndpointKey = (name: string, method: string) => `${name}${URL_DIVIDER}${method}`;
+
+const DISPLAYED_MEDIA_TYPES = ['application/json', 'application/merge-patch+json'];
+
+export function extractSchemaAndMediaType(
+ content?: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>,
+) {
+ if (!content) {
+ return [];
+ }
+
+ const requests = Object.keys(content)
+ .filter((mediaType) => DISPLAYED_MEDIA_TYPES.includes(mediaType))
+ .map((requestMediaType) => {
+ const schema = content[requestMediaType]?.schema;
+
+ if (!schema) {
+ return null;
+ }
+
+ return {
+ requestMediaType,
+ schema: JSON.stringify(mapOpenAPISchema(schema), null, 2),
+ };
+ })
+ .filter(isDefined);
+
+ return requests;
+}