Browse Source

SONAR-21054 Add Request Body details for PATCH and POST in WEB API v2 Doc

tags/10.4.0.87286
guillaume-peoch-sonarsource 7 months ago
parent
commit
baf76207ef

+ 2
- 1
server/sonar-web/src/main/js/api/mocks/data/web-api.ts View File

@@ -659,7 +659,7 @@ export const openApiTestData: OpenAPIV3.Document = {
type: 'object',
properties: {
id: { type: 'integer', format: 'int64', example: 10 },
name: { type: 'string', example: 'doggie' },
name: { type: 'string', minLength: 3, maxLength: 100, example: 'doggie' },
category: { $ref: '#/components/schemas/Category' },
photoUrls: {
type: 'array',
@@ -675,6 +675,7 @@ export const openApiTestData: OpenAPIV3.Document = {
type: 'string',
description: 'pet status in the store',
enum: ['available', 'pending', 'sold'],
deprecated: true,
},
},
xml: { name: 'pet' },

+ 23
- 0
server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx View File

@@ -46,6 +46,9 @@ const ui = {
requestHeader: byRole('list', { name: 'api_documentation.v2.request_subheader.header' }).byRole(
'listitem',
),
requestBodyParameter: byRole('list', {
name: 'api_documentation.v2.request_subheader.request_body',
}).byRole('listitem'),
response: byRole('list', { name: 'api_documentation.v2.response_header' }).byRole('listitem'),
};

@@ -96,6 +99,22 @@ it('should navigate between apis', async () => {
expect(ui.pathParameter.query()).not.toBeInTheDocument();
expect(ui.requestHeader.query()).not.toBeInTheDocument();
expect(ui.requestBody.get()).toBeInTheDocument();
expect(ui.requestBodyParameter.getAll()).toHaveLength(6);
expect(ui.requestBodyParameter.byRole('button').getAt(0)).toHaveAttribute(
'aria-expanded',
'false',
);
await user.click(ui.requestBodyParameter.byRole('button').getAt(0));
expect(ui.requestBodyParameter.byRole('button').getAt(0)).toHaveAttribute(
'aria-expanded',
'true',
);
expect(ui.requestBodyParameter.getAt(0)).toHaveTextContent('name requiredmax: 100min: 3');
await user.click(ui.requestBodyParameter.byRole('button').getAt(0));
expect(ui.requestBodyParameter.byRole('button').getAt(0)).toHaveAttribute(
'aria-expanded',
'false',
);
expect(ui.response.byRole('button').getAt(0)).toHaveAttribute('aria-expanded', 'true');
expect(ui.response.byRole('button').getAt(2)).toHaveAttribute('aria-expanded', 'false');
expect(ui.response.getAt(0)).toHaveTextContent('200Successful operation');
@@ -138,6 +157,10 @@ it('should navigate between apis', async () => {
expect(ui.queryParameter.getAt(1)).not.toHaveTextContent('deprecated');
await user.click(ui.queryParameter.byRole('button').getAt(1));
expect(ui.queryParameter.getAt(1)).toHaveTextContent('max: 5min: -1example: 3');

await user.click(ui.apiSidebarItem.getAt(7));
expect(await screen.findByText('/api/v3/pet/{petId}/uploadImage')).toBeInTheDocument();
expect(screen.getByText('no_data')).toBeInTheDocument();
});

it('should show About page', async () => {

+ 12
- 10
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx View File

@@ -25,6 +25,7 @@ import React from 'react';
import { translate } from '../../../helpers/l10n';
import { ExcludeReferences } from '../types';
import { mapOpenAPISchema } from '../utils';
import ApiRequestBodyParameters from './ApiRequestParameters';
import ApiResponseSchema from './ApiResponseSchema';

interface Props {
@@ -53,15 +54,6 @@ export default function ApiParameters({ data }: Props) {
return (
<>
<SubTitle>{translate('api_documentation.v2.parameter_header')}</SubTitle>
{!requestBody && !data.parameters?.length && <TextMuted text={translate('no_data')} />}
{requestBody && (
<div>
<SubHeading>
{translate('api_documentation.v2.request_subheader.request_body')}
</SubHeading>
<ApiResponseSchema content={requestBody} />
</div>
)}
{Object.entries(groupBy(data.parameters, (p) => p.in)).map(([group, parameters]) => (
<div key={group}>
<SubHeading id={`api-parameters-${group}`}>
@@ -103,7 +95,7 @@ export default function ApiParameters({ data }: Props) {
text={`${translate('max')}: ${parameter.schema?.maximum}`}
/>
)}
{parameter.schema?.minimum && (
{typeof parameter.schema?.minimum === 'number' && (
<TextMuted
className="sw-mt-2 sw-block"
text={`${translate('min')}: ${parameter.schema?.minimum}`}
@@ -127,6 +119,16 @@ export default function ApiParameters({ data }: Props) {
</ul>
</div>
))}
{!requestBody && !data.parameters?.length && <TextMuted text={translate('no_data')} />}
{requestBody && (
<div>
<SubHeading id="api_documentation.v2.request_subheader.request_body">
{translate('api_documentation.v2.request_subheader.request_body')}
</SubHeading>
<ApiResponseSchema content={requestBody} />
<ApiRequestBodyParameters content={requestBody} />
</div>
)}
</>
);
}

+ 112
- 0
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestParameters.tsx View File

@@ -0,0 +1,112 @@
/*
* 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 { Accordion, Badge, TextMuted } from 'design-system';
import { isEmpty } from 'lodash';
import { OpenAPIV3 } from 'openapi-types';
import React from 'react';
import { translate } from '../../../helpers/l10n';
import { ExcludeReferences } from '../types';

interface Props {
content?: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>;
}

export default function ApiRequestBodyParameters({ content }: Readonly<Props>) {
const [openParameters, setOpenParameters] = React.useState<string[]>([]);

const toggleParameter = (parameter: string) => {
if (openParameters.includes(parameter)) {
setOpenParameters(openParameters.filter((n) => n !== parameter));
} else {
setOpenParameters([...openParameters, parameter]);
}
};

const schema =
content &&
(content['application/json']?.schema || content['application/merge-patch+json']?.schema);

if (!schema?.properties || schema?.type !== 'object' || isEmpty(schema?.properties)) {
return null;
}

const parameters = schema.properties;
const required = schema.required ?? [];

const orderedKeys = Object.keys(parameters).sort((a, b) => {
if (required?.includes(a) && !required?.includes(b)) {
return -1;
}
if (!required?.includes(a) && required?.includes(b)) {
return 1;
}
return 0;
});

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].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>
);
})}
</ul>
);
}

Loading…
Cancel
Save