From: Viktor Vorona Date: Mon, 24 Jul 2023 11:41:28 +0000 (+0200) Subject: SONAR-19968 Web API v2 Documentation X-Git-Tag: 10.2.0.77647~275 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=5f30a8ab495390c57c12ccab5fc228b4591a23ed;p=sonarqube.git SONAR-19968 Web API v2 Documentation --- diff --git a/server/sonar-web/design-system/src/components/Accordion.tsx b/server/sonar-web/design-system/src/components/Accordion.tsx index 84fcdf41571..b6a97708988 100644 --- a/server/sonar-web/design-system/src/components/Accordion.tsx +++ b/server/sonar-web/design-system/src/components/Accordion.tsx @@ -17,9 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; import classNames from 'classnames'; import { uniqueId } from 'lodash'; -import * as React from 'react'; +import React from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers'; +import { ThemedProps } from '../types'; import { BareButton } from './buttons'; import { OpenCloseIndicator } from './icons/OpenCloseIndicator'; @@ -42,7 +47,7 @@ export function Accordion(props: AccordionProps) { }, [onClick, data]); return ( -
@@ -60,8 +65,53 @@ export function Accordion(props: AccordionProps) {
- {open &&
{props.children}
} + {open &&
{props.children}
}
-
+ ); } + +const accordionStyle = (props: ThemedProps) => css` + box-sizing: border-box; + text-decoration: none; + outline: none; + border: ${themeBorder('default', 'buttonSecondaryBorder')(props)}; + color: ${themeContrast('buttonSecondary')(props)}; + background-color: ${themeColor('buttonSecondary')(props)}; + transition: background-color 0.2s ease, outline 0.2s ease; + + & > button { + ${tw`sw-body-sm-highlight`} + } + ${tw`sw-rounded-2`} + ${tw`sw-overflow-hidden`} + ${tw`sw-cursor-pointer`} + + & > button:hover, & > button:active { + color: ${themeContrast('buttonSecondary')(props)}; + background-color: ${themeColor('buttonSecondaryHover')(props)}; + } + + & > button:focus, + & > button:active { + color: ${themeContrast('buttonSecondary')(props)}; + outline: ${themeBorder('focus', 'var(--focus)')(props)}; + } + + & > button:disabled, + & > button:disabled:hover { + color: ${themeContrast('buttonDisabled')(props)}; + background-color: ${themeColor('buttonDisabled')(props)}; + border: ${themeBorder('default', 'buttonDisabledBorder')(props)}; + + ${tw`sw-cursor-not-allowed`} + } + + & > svg { + ${tw`sw-mr-1`} + } +`; + +const Container = styled.div` + ${accordionStyle} +`; diff --git a/server/sonar-web/design-system/src/components/BorderlessAccordion.tsx b/server/sonar-web/design-system/src/components/BorderlessAccordion.tsx new file mode 100644 index 00000000000..57657084038 --- /dev/null +++ b/server/sonar-web/design-system/src/components/BorderlessAccordion.tsx @@ -0,0 +1,67 @@ +/* + * 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 { uniqueId } from 'lodash'; +import * as React from 'react'; +import { BareButton } from './buttons'; +import { OpenCloseIndicator } from './icons/OpenCloseIndicator'; + +interface AccordionProps { + ariaLabel?: string; + children: React.ReactNode; + className?: string; + data?: string; + header: React.ReactNode; + onClick: (data?: string) => void; + open: boolean; +} + +export function BorderlessAccordion(props: AccordionProps) { + const { ariaLabel, className, open, header, data, onClick } = props; + + const id = React.useMemo(() => uniqueId('accordion-'), []); + const handleClick = React.useCallback(() => { + onClick(data); + }, [onClick, data]); + + return ( +
+ + {header} + + +
+ {open &&
{props.children}
} +
+
+ ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Accordion-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Accordion-test.tsx deleted file mode 100644 index 323175c072c..00000000000 --- a/server/sonar-web/design-system/src/components/__tests__/Accordion-test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import * as React from 'react'; -import { render } from '../../helpers/testUtils'; -import { Accordion } from '../Accordion'; - -it('should behave correctly', async () => { - const user = userEvent.setup(); - const children = 'hello'; - renderAccordion(children); - expect(screen.queryByText(children)).not.toBeInTheDocument(); - await user.click(screen.getByRole('button', { expanded: false })); - expect(screen.getByText(children)).toBeInTheDocument(); -}); - -function renderAccordion(children: React.ReactNode) { - function AccordionTest() { - const [open, setOpen] = React.useState(false); - - return ( - { - setOpen(!open); - }} - open={open} - > -
{children}
-
- ); - } - - return render(); -} diff --git a/server/sonar-web/design-system/src/components/__tests__/BorderlessAccordion-test.tsx b/server/sonar-web/design-system/src/components/__tests__/BorderlessAccordion-test.tsx new file mode 100644 index 00000000000..7598cf3e96c --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/BorderlessAccordion-test.tsx @@ -0,0 +1,53 @@ +/* + * 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 { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; +import { render } from '../../helpers/testUtils'; +import { BorderlessAccordion } from '../BorderlessAccordion'; + +it('should behave correctly', async () => { + const user = userEvent.setup(); + const children = 'hello'; + renderAccordion(children); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + await user.click(screen.getByRole('button', { expanded: false })); + expect(screen.getByText(children)).toBeInTheDocument(); +}); + +function renderAccordion(children: React.ReactNode) { + function AccordionTest() { + const [open, setOpen] = React.useState(false); + + return ( + { + setOpen(!open); + }} + open={open} + > +
{children}
+
+ ); + } + + return render(); +} diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 0e4783df607..5536c8be6cd 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -23,6 +23,7 @@ export * from './Avatar'; export { Badge } from './Badge'; export * from './Banner'; export { BarChart } from './BarChart'; +export * from './BorderlessAccordion'; export { Breadcrumbs } from './Breadcrumbs'; export * from './BubbleChart'; export * from './Card'; diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index dfdd3cf7092..c7808145a30 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -100,6 +100,7 @@ "jest-environment-jsdom": "29.5.0", "jest-junit": "16.0.0", "jsdom": "21.1.1", + "openapi-types": "12.1.3", "path-browserify": "1.0.1", "postcss": "8.4.24", "postcss-calc": "9.0.1", diff --git a/server/sonar-web/src/main/js/api/mocks/WebApiServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/WebApiServiceMock.ts new file mode 100644 index 00000000000..c6f5fc9a177 --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/WebApiServiceMock.ts @@ -0,0 +1,43 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import { fetchOpenAPI } from '../web-api'; +import { openApiTestData } from './data/web-api'; + +jest.mock('../web-api'); + +export default class WebApiServiceMock { + openApiDocument: OpenAPIV3.Document; + + constructor() { + this.openApiDocument = cloneDeep(openApiTestData); + + jest.mocked(fetchOpenAPI).mockImplementation(this.handleFetchOpenAPI); + } + + handleFetchOpenAPI: typeof fetchOpenAPI = () => { + return Promise.resolve(this.openApiDocument); + }; + + reset = () => { + this.openApiDocument = cloneDeep(openApiTestData); + }; +} diff --git a/server/sonar-web/src/main/js/api/mocks/data/web-api.ts b/server/sonar-web/src/main/js/api/mocks/data/web-api.ts new file mode 100644 index 00000000000..1e5b475ed78 --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/data/web-api.ts @@ -0,0 +1,722 @@ +/* + * 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 { OpenAPIV3 } from 'openapi-types'; + +export const openApiTestData: OpenAPIV3.Document = { + openapi: '3.0.2', + info: { + title: 'Swagger Petstore - OpenAPI 3.0', + description: + "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + termsOfService: 'http://swagger.io/terms/', + contact: { email: 'apiteam@swagger.io' }, + license: { name: 'Apache 2.0', url: 'http://www.apache.org/licenses/LICENSE-2.0.html' }, + version: '1.0.17', + }, + externalDocs: { description: 'Find out more about Swagger', url: 'http://swagger.io' }, + servers: [{ url: '/api/v3' }], + tags: [ + { + name: 'pet', + description: 'Everything about your Pets', + externalDocs: { description: 'Find out more', url: 'http://swagger.io' }, + }, + { + name: 'store', + description: 'Access to Petstore orders', + externalDocs: { description: 'Find out more about our store', url: 'http://swagger.io' }, + }, + { name: 'user', description: 'Operations about user' }, + ], + paths: { + '/pet': { + put: { + tags: ['pet'], + summary: 'Update an existing pet', + description: 'Update an existing pet by Id', + operationId: 'updatePet', + requestBody: { + description: 'Update an existent pet in the store', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/x-www-form-urlencoded': { + schema: { $ref: '#/components/schemas/Pet' }, + }, + }, + required: true, + }, + responses: { + '200': { + description: 'Successful operation', + content: { + 'application/xml': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/json': { schema: { $ref: '#/components/schemas/Pet' } }, + }, + }, + '400': { description: 'Invalid ID supplied' }, + '404': { description: 'Pet not found' }, + '405': { description: 'Validation exception' }, + }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + post: { + tags: ['pet'], + summary: 'Add a new pet to the store', + description: 'Add a new pet to the store', + operationId: 'addPet', + requestBody: { + description: 'Create a new pet in the store', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/x-www-form-urlencoded': { + schema: { $ref: '#/components/schemas/Pet' }, + }, + }, + required: true, + }, + responses: { + '200': { + description: 'Successful operation', + content: { + 'application/xml': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/json': { schema: { $ref: '#/components/schemas/Pet' } }, + }, + }, + '405': { description: 'Invalid input' }, + }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + }, + '/pet/findByStatus': { + get: { + tags: ['pet'], + summary: 'Finds Pets by status', + description: 'Multiple status values can be provided with comma separated strings', + operationId: 'findPetsByStatus', + parameters: [ + { + name: 'status', + in: 'query', + description: 'Status values that need to be considered for filter', + required: false, + explode: true, + schema: { + type: 'string', + default: 'available', + enum: ['available', 'pending', 'sold'], + }, + }, + ], + responses: { + '200': { + description: 'successful operation', + content: { + 'application/xml': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Pet' } }, + }, + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Pet' } }, + }, + }, + }, + '400': { description: 'Invalid status value' }, + }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + }, + '/pet/findByTags': { + get: { + tags: ['pet'], + summary: 'Finds Pets by tags', + description: + 'Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.', + operationId: 'findPetsByTags', + parameters: [ + { + name: 'tags', + in: 'query', + description: 'Tags to filter by', + required: false, + explode: true, + schema: { type: 'array', items: { type: 'string' } }, + }, + ], + responses: { + '200': { + description: 'successful operation', + content: { + 'application/xml': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Pet' } }, + }, + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Pet' } }, + }, + }, + }, + '400': { description: 'Invalid tag value' }, + }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + }, + '/pet/{petId}': { + get: { + tags: ['pet'], + summary: 'Find pet by ID', + description: 'Returns a single pet', + operationId: 'getPetById', + parameters: [ + { + name: 'petId', + in: 'path', + description: 'ID of pet to return', + required: true, + schema: { type: 'integer', format: 'int64' }, + }, + ], + responses: { + '200': { + description: 'successful operation', + content: { + 'application/xml': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/json': { schema: { $ref: '#/components/schemas/Pet' } }, + }, + }, + '400': { description: 'Invalid ID supplied' }, + '404': { description: 'Pet not found' }, + }, + security: [{ api_key: [] }, { petstore_auth: ['write:pets', 'read:pets'] }], + }, + post: { + tags: ['pet'], + summary: 'Updates a pet in the store with form data', + description: '', + operationId: 'updatePetWithForm', + parameters: [ + { + name: 'petId', + in: 'path', + description: 'ID of pet that needs to be updated', + required: true, + schema: { type: 'integer', format: 'int64' }, + }, + { + name: 'name', + in: 'query', + description: 'Name of pet that needs to be updated', + deprecated: true, + schema: { type: 'string' }, + }, + { + name: 'status', + in: 'query', + description: 'Status of pet that needs to be updated', + example: 3, + schema: { type: 'integer', format: 'int32', maximum: 5, minimum: -1 }, + }, + ], + responses: { '405': { description: 'Invalid input' } }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + delete: { + tags: ['pet'], + summary: 'Deletes a pet', + description: '', + operationId: 'deletePet', + parameters: [ + { + name: 'api_key', + in: 'header', + description: '', + required: false, + schema: { type: 'string' }, + }, + { + name: 'petId', + in: 'path', + description: 'Pet id to delete', + required: true, + schema: { type: 'integer', format: 'int64' }, + }, + ], + responses: { '400': { description: 'Invalid pet value' } }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + }, + '/pet/{petId}/uploadImage': { + post: { + tags: ['pet'], + summary: 'uploads an image', + description: '', + operationId: 'uploadFile', + parameters: [ + { + name: 'petId', + in: 'path', + description: 'ID of pet to update', + required: true, + schema: { type: 'integer', format: 'int64' }, + }, + { + name: 'additionalMetadata', + in: 'query', + description: 'Additional Metadata', + required: false, + schema: { type: 'string' }, + }, + ], + requestBody: { + content: { + 'application/octet-stream': { schema: { type: 'string', format: 'binary' } }, + }, + }, + responses: { + '200': { + description: 'successful operation', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/ApiResponse' } }, + }, + }, + }, + security: [{ petstore_auth: ['write:pets', 'read:pets'] }], + }, + }, + '/store/inventory': { + get: { + tags: ['store'], + summary: 'Returns pet inventories by status', + description: 'Returns a map of status codes to quantities', + operationId: 'getInventory', + responses: { + '200': { + description: 'successful operation', + content: { + 'application/json': { + schema: { + type: 'object', + additionalProperties: { type: 'integer', format: 'int32' }, + }, + }, + }, + }, + }, + security: [{ api_key: [] }], + }, + }, + '/store/order': { + post: { + tags: ['store'], + summary: 'Place an order for a pet', + description: 'Place a new order in the store', + operationId: 'placeOrder', + requestBody: { + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Order' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/Order' } }, + 'application/x-www-form-urlencoded': { + schema: { $ref: '#/components/schemas/Order' }, + }, + }, + }, + responses: { + '200': { + description: 'successful operation', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Order' } }, + }, + }, + '405': { description: 'Invalid input' }, + }, + }, + }, + '/store/order/{orderId}': { + get: { + tags: ['store'], + summary: 'Find purchase order by ID', + description: + 'For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.', + operationId: 'getOrderById', + parameters: [ + { + name: 'orderId', + in: 'path', + description: 'ID of order that needs to be fetched', + required: true, + schema: { type: 'integer', format: 'int64' }, + }, + ], + responses: { + '200': { + description: 'successful operation', + content: { + 'application/xml': { schema: { $ref: '#/components/schemas/Order' } }, + 'application/json': { schema: { $ref: '#/components/schemas/Order' } }, + }, + }, + '400': { description: 'Invalid ID supplied' }, + '404': { description: 'Order not found' }, + }, + }, + delete: { + tags: ['store'], + summary: 'Delete purchase order by ID', + description: + 'For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors', + operationId: 'deleteOrder', + parameters: [ + { + name: 'orderId', + in: 'path', + description: 'ID of the order that needs to be deleted', + required: true, + schema: { type: 'integer', format: 'int64' }, + }, + ], + responses: { + '400': { description: 'Invalid ID supplied' }, + '404': { description: 'Order not found' }, + }, + }, + }, + '/user': { + post: { + tags: ['user'], + summary: 'Create user', + description: 'This can only be done by the logged in user.', + operationId: 'createUser', + requestBody: { + description: 'Created user object', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/User' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/User' } }, + 'application/x-www-form-urlencoded': { + schema: { $ref: '#/components/schemas/User' }, + }, + }, + }, + responses: { + default: { + description: 'successful operation', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/User' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/User' } }, + }, + }, + }, + }, + }, + '/user/createWithList': { + post: { + tags: ['user'], + summary: 'Creates list of users with given input array', + description: 'Creates list of users with given input array', + operationId: 'createUsersWithListInput', + requestBody: { + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/User' } }, + }, + }, + }, + responses: { + '200': { + description: 'Successful operation', + content: { + 'application/xml': { schema: { $ref: '#/components/schemas/User' } }, + 'application/json': { schema: { $ref: '#/components/schemas/User' } }, + }, + }, + default: { description: 'successful operation' }, + }, + }, + }, + '/user/login': { + get: { + tags: ['user'], + summary: 'Logs user into the system', + description: '', + operationId: 'loginUser', + parameters: [ + { + name: 'username', + in: 'query', + description: 'The user name for login', + required: false, + schema: { type: 'string' }, + }, + { + name: 'password', + in: 'query', + description: 'The password for login in clear text', + required: false, + schema: { type: 'string' }, + }, + ], + responses: { + '200': { + description: 'successful operation', + headers: { + 'X-Rate-Limit': { + description: 'calls per hour allowed by the user', + schema: { type: 'integer', format: 'int32' }, + }, + 'X-Expires-After': { + description: 'date in UTC when token expires', + schema: { type: 'string', format: 'date-time' }, + }, + }, + content: { + 'application/xml': { schema: { type: 'string' } }, + 'application/json': { schema: { type: 'string' } }, + }, + }, + '400': { description: 'Invalid username/password supplied' }, + }, + }, + }, + '/user/logout': { + get: { + tags: ['user'], + summary: 'Logs out current logged in user session', + description: '', + operationId: 'logoutUser', + parameters: [], + responses: { default: { description: 'successful operation' } }, + }, + }, + '/user/{username}': { + get: { + tags: ['user'], + summary: 'Get user by user name', + description: '', + operationId: 'getUserByName', + parameters: [ + { + name: 'username', + in: 'path', + description: 'The name that needs to be fetched. Use user1 for testing. ', + required: true, + schema: { type: 'string' }, + }, + ], + responses: { + '200': { + description: 'successful operation', + content: { + 'application/xml': { schema: { $ref: '#/components/schemas/User' } }, + 'application/json': { schema: { $ref: '#/components/schemas/User' } }, + }, + }, + '400': { description: 'Invalid username supplied' }, + '404': { description: 'User not found' }, + }, + }, + put: { + tags: ['user'], + summary: 'Update user', + description: 'This can only be done by the logged in user.', + operationId: 'updateUser', + parameters: [ + { + name: 'username', + in: 'path', + description: 'name that need to be deleted', + required: true, + schema: { type: 'string' }, + }, + ], + requestBody: { + description: 'Update an existent user in the store', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/User' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/User' } }, + 'application/x-www-form-urlencoded': { + schema: { $ref: '#/components/schemas/User' }, + }, + }, + }, + responses: { default: { description: 'successful operation' } }, + }, + delete: { + tags: ['user'], + summary: 'Delete user', + description: 'This can only be done by the logged in user.', + operationId: 'deleteUser', + parameters: [ + { + name: 'username', + in: 'path', + description: 'The name that needs to be deleted', + required: true, + schema: { type: 'string' }, + }, + ], + responses: { + '400': { description: 'Invalid username supplied' }, + '404': { description: 'User not found' }, + }, + }, + }, + }, + components: { + schemas: { + Order: { + type: 'object', + properties: { + id: { type: 'integer', format: 'int64', example: 10 }, + petId: { type: 'integer', format: 'int64', example: 198772 }, + quantity: { type: 'integer', format: 'int32', example: 7 }, + shipDate: { type: 'string', format: 'date-time' }, + status: { + type: 'string', + description: 'Order Status', + example: 'approved', + enum: ['placed', 'approved', 'delivered'], + }, + complete: { type: 'boolean' }, + }, + xml: { name: 'order' }, + }, + Customer: { + type: 'object', + properties: { + id: { type: 'integer', format: 'int64', example: 100000 }, + username: { type: 'string', example: 'fehguy' }, + address: { + type: 'array', + xml: { name: 'addresses', wrapped: true }, + items: { $ref: '#/components/schemas/Address' }, + }, + }, + xml: { name: 'customer' }, + }, + Address: { + type: 'object', + properties: { + street: { type: 'string', example: '437 Lytton' }, + city: { type: 'string', example: 'Palo Alto' }, + state: { type: 'string', example: 'CA' }, + zip: { type: 'string', example: '94301' }, + }, + xml: { name: 'address' }, + }, + Category: { + type: 'object', + properties: { + id: { type: 'integer', format: 'int64', example: 1 }, + name: { type: 'string', example: 'Dogs' }, + }, + xml: { name: 'category' }, + }, + User: { + type: 'object', + properties: { + id: { type: 'integer', format: 'int64', example: 10 }, + username: { type: 'string', example: 'theUser' }, + firstName: { type: 'string', example: 'John' }, + lastName: { type: 'string', example: 'James' }, + email: { type: 'string', example: 'john@email.com' }, + password: { type: 'string', example: '12345' }, + phone: { type: 'string', example: '12345' }, + userStatus: { + type: 'integer', + description: 'User Status', + format: 'int32', + example: 1, + }, + }, + xml: { name: 'user' }, + }, + Tag: { + type: 'object', + properties: { + id: { type: 'integer', format: 'int64' }, + name: { type: 'string' }, + }, + xml: { name: 'tag' }, + }, + Pet: { + required: ['name', 'photoUrls'], + type: 'object', + properties: { + id: { type: 'integer', format: 'int64', example: 10 }, + name: { type: 'string', example: 'doggie' }, + category: { $ref: '#/components/schemas/Category' }, + photoUrls: { + type: 'array', + xml: { wrapped: true }, + items: { type: 'string', xml: { name: 'photoUrl' } }, + }, + tags: { + type: 'array', + xml: { wrapped: true }, + items: { $ref: '#/components/schemas/Tag' }, + }, + status: { + type: 'string', + description: 'pet status in the store', + enum: ['available', 'pending', 'sold'], + }, + }, + xml: { name: 'pet' }, + }, + ApiResponse: { + type: 'object', + properties: { + code: { type: 'integer', format: 'int32' }, + type: { type: 'string' }, + message: { type: 'string' }, + }, + xml: { name: '##default' }, + }, + }, + requestBodies: { + Pet: { + description: 'Pet object that needs to be added to the store', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Pet' } }, + 'application/xml': { schema: { $ref: '#/components/schemas/Pet' } }, + }, + }, + UserArray: { + description: 'List of user object', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/User' } }, + }, + }, + }, + }, + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://petstore3.swagger.io/oauth/authorize', + scopes: { 'write:pets': 'modify pets in your account', 'read:pets': 'read your pets' }, + }, + }, + }, + api_key: { type: 'apiKey', name: 'api_key', in: 'header' }, + }, + }, +}; diff --git a/server/sonar-web/src/main/js/api/web-api.ts b/server/sonar-web/src/main/js/api/web-api.ts index 2e65bd5e3ef..42240ee3237 100644 --- a/server/sonar-web/src/main/js/api/web-api.ts +++ b/server/sonar-web/src/main/js/api/web-api.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { OpenAPIV3 } from 'openapi-types'; import { throwGlobalError } from '../helpers/error'; import { getJSON } from '../helpers/request'; import { WebApi } from '../types/types'; @@ -41,3 +42,7 @@ export function fetchResponseExample(domain: string, action: string): Promise { + return getJSON('/api/v2/api-docs').catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index f4924fa10b7..a974c4bfba1 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -47,6 +47,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [ '/project/extension/securityreport/securityreport', '/projects', '/project/information', + '/web_api_v2', ]; const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = ['/tutorials']; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 1664ad7abcc..def7cb998a2 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -59,6 +59,7 @@ import settingsRoutes from '../../apps/settings/routes'; import systemRoutes from '../../apps/system/routes'; import tutorialsRoutes from '../../apps/tutorials/routes'; import usersRoutes from '../../apps/users/routes'; +import webAPIRoutesV2 from '../../apps/web-api-v2/routes'; import webAPIRoutes from '../../apps/web-api/routes'; import webhooksRoutes from '../../apps/webhooks/routes'; import { translate } from '../../helpers/l10n'; @@ -231,6 +232,7 @@ export default function startReactApp( } /> {webAPIRoutes()} + {webAPIRoutesV2()} {renderComponentRoutes()} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx index 6a3379d9de9..76f60db5dea 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Accordion, BasicSeparator, TextMuted } from 'design-system'; +import { BasicSeparator, BorderlessAccordion, TextMuted } from 'design-system'; import * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; @@ -134,7 +134,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { <> {isApplication ? ( <> - {renderFailedConditions()} - + {(!isLastStatus || collapsed) && } 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 new file mode 100644 index 00000000000..94d840db9b2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx @@ -0,0 +1,111 @@ +/* + * 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 styled from '@emotion/styled'; +import { DeferredSpinner, LargeCenteredLayout, PageContentFontWrapper, Title } from 'design-system'; +import { omit } from 'lodash'; +import React, { useMemo } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { useLocation } from 'react-router-dom'; +import { translate } from '../../helpers/l10n'; +import ApiInformation from './components/ApiInformation'; +import ApiSidebar from './components/ApiSidebar'; +import { useOpenAPI } from './queries'; +import { URL_DIVIDER, dereferenceSchema } from './utils'; + +export default function WebApiApp() { + const { data, isLoading } = useOpenAPI(); + const location = useLocation(); + const activeApi = location.hash.replace('#', '').split(URL_DIVIDER); + + const apis = useMemo(() => { + if (!data) { + return []; + } + return Object.entries(dereferenceSchema(data).paths ?? {}).reduce( + (acc, [name, methods]) => [ + ...acc, + ...Object.entries( + omit(methods, 'summary', '$ref', 'description', 'servers', 'parameters') ?? {} + ).map(([method, info]) => ({ name, method, info })), + ], + [] + ); + }, [data]); + + const activeData = + activeApi.length > 1 && + apis.find((api) => api.name === activeApi[0] && api.method === activeApi[1]); + + return ( + + + + + {data && ( +
+ +
+ ({ + method, + name, + info, + }))} + /> +
+
+
+ + {!activeData && ( + <> + {translate('about')} +

{data.info.description}

+ + )} + {data && activeData && ( + + )} +
+
+
+ )} +
+
+
+ ); +} + +const NavContainer = styled.nav` + scrollbar-gutter: stable; + overflow-y: auto; + overflow-x: hidden; + height: calc(100vh - 160px); + padding-top: 1.5rem; + padding-bottom: 1.5rem; +`; 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 new file mode 100644 index 00000000000..e3a795cfd6d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/WebApiApp-it.tsx @@ -0,0 +1,162 @@ +/* + * 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 { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; +import WebApiServiceMock from '../../../api/mocks/WebApiServiceMock'; +import { renderApp } from '../../../helpers/testReactTestingUtils'; +import { byRole, byTestId, byText } from '../../../helpers/testSelector'; +import WebApiApp from '../WebApiApp'; + +const handler = new WebApiServiceMock(); + +const ui = { + search: byRole('searchbox'), + title: byRole('link', { name: 'Swagger Petstore - OpenAPI 3.0 1.0.17' }), + searchClear: byRole('button', { name: 'clear' }), + apiScopePet: byRole('button', { name: 'pet' }), + apiScopeStore: byRole('button', { name: 'store' }), + apiScopeUser: byRole('button', { name: 'user' }), + 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( + 'listitem' + ), + pathParameter: byRole('list', { name: 'api_documentation.v2.request_subheader.path' }).byRole( + 'listitem' + ), + requestHeader: byRole('list', { name: 'api_documentation.v2.request_subheader.header' }).byRole( + 'listitem' + ), + response: byRole('list', { name: 'api_documentation.v2.response_header' }).byRole('listitem'), +}; + +beforeEach(() => { + handler.reset(); +}); + +it('should search apis', async () => { + const user = userEvent.setup(); + renderWebApiApp(); + expect(await ui.apiScopePet.find()).toBeInTheDocument(); + expect(ui.apiScopeStore.get()).toBeInTheDocument(); + expect(ui.apiScopeUser.get()).toBeInTheDocument(); + expect(ui.apiSidebarItem.queryAll()).toHaveLength(0); + + await user.click(ui.apiScopePet.get()); + expect(ui.apiSidebarItem.getAll()).toHaveLength(8); + await user.click(ui.apiScopeStore.get()); + expect(ui.apiSidebarItem.getAll()).toHaveLength(12); + await user.click(ui.apiScopeUser.get()); + expect(ui.apiSidebarItem.getAll()).toHaveLength(19); + + await user.type(ui.search.get(), 'put'); + const putItems = ui.apiSidebarItem.getAll(); + expect(putItems).toHaveLength(3); + expect(ui.apiScopePet.get()).toBeInTheDocument(); + expect(ui.apiScopeStore.query()).not.toBeInTheDocument(); + expect(ui.apiScopeUser.get()).toBeInTheDocument(); + expect(putItems[0]).toHaveTextContent('PUTUpdate an existing pet'); + expect(putItems[1]).toHaveTextContent('POSTCreates list of users with given input array'); + + await user.click(ui.searchClear.get()); + + expect(ui.apiScopeStore.get()).toBeInTheDocument(); + expect(ui.apiSidebarItem.getAll().length).toBeGreaterThan(3); +}); + +it('should navigate between apis', async () => { + const user = userEvent.setup(); + renderWebApiApp(); + await user.click(await ui.apiScopePet.find()); + + await user.click(ui.apiSidebarItem.getAt(0)); + expect(await screen.findByText('/api/v3/pet')).toBeInTheDocument(); + expect(await screen.findByText('Update an existing pet by Id')).toBeInTheDocument(); + expect(ui.response.getAll()).toHaveLength(4); + expect(ui.queryParameter.query()).not.toBeInTheDocument(); + expect(ui.pathParameter.query()).not.toBeInTheDocument(); + expect(ui.requestHeader.query()).not.toBeInTheDocument(); + expect(ui.requestBody.get()).toBeInTheDocument(); + 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'); + expect(ui.response.getAt(0)).toHaveTextContent('"name": "string"'); + expect(ui.response.getAt(1)).not.toHaveTextContent('no_data'); + await user.click(ui.response.byRole('button').getAt(2)); + expect(ui.response.getAt(0)).toHaveTextContent('"name": "string"'); + expect(ui.response.getAt(1)).toHaveTextContent('no_data'); + await user.click(ui.response.byRole('button').getAt(0)); + expect(ui.response.getAt(0)).not.toHaveTextContent('"name": "string"'); + expect(ui.response.getAt(1)).toHaveTextContent('no_data'); + + await user.click(ui.apiSidebarItem.getAt(2)); + expect(await screen.findByText('/api/v3/pet/findByStatus')).toBeInTheDocument(); + expect(ui.response.byRole('button').getAt(0)).toHaveAttribute('aria-expanded', 'true'); + expect(ui.response.byRole('button').getAt(2)).toHaveAttribute('aria-expanded', 'false'); + expect(ui.queryParameter.get()).toBeInTheDocument(); + expect(ui.pathParameter.query()).not.toBeInTheDocument(); + expect(ui.requestHeader.query()).not.toBeInTheDocument(); + expect(ui.requestBody.query()).not.toBeInTheDocument(); + expect(ui.queryParameter.getAll()).toHaveLength(1); + expect(ui.queryParameter.getAt(0)).toHaveTextContent('status ["available","pending","sold"]'); + expect(ui.queryParameter.getAt(0)).not.toHaveTextContent('default: available'); + await user.click(ui.queryParameter.byRole('button').getAt(0)); + expect(ui.queryParameter.getAt(0)).toHaveTextContent('default: available'); + 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)); + expect(await screen.findByText('/api/v3/pet/{petId}')).toBeInTheDocument(); + expect(ui.queryParameter.getAll()).toHaveLength(2); + expect(ui.pathParameter.getAll()).toHaveLength(1); + expect(ui.requestHeader.query()).not.toBeInTheDocument(); + expect(ui.requestBody.query()).not.toBeInTheDocument(); + expect(ui.pathParameter.getAt(0)).toHaveTextContent('petId integer (int64)required'); + expect(ui.pathParameter.getAt(0)).not.toHaveTextContent('ID of pet that needs to be updated'); + await user.click(ui.pathParameter.byRole('button').getAt(0)); + expect(ui.pathParameter.getAt(0)).toHaveTextContent('ID of pet that needs to be updated'); + expect(ui.queryParameter.getAt(0)).toHaveTextContent('deprecated'); + 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'); +}); + +it('should show About page', async () => { + const user = userEvent.setup(); + renderWebApiApp(); + expect(await screen.findByText('about')).toBeInTheDocument(); + expect( + screen.getByText('This is a sample Pet Store Server based on the OpenAPI 3.0 specification.', { + exact: false, + }) + ).toBeInTheDocument(); + await user.click(ui.apiScopePet.get()); + await user.click(ui.apiSidebarItem.getAt(0)); + expect(screen.queryByText('about')).not.toBeInTheDocument(); + await user.click(ui.title.get()); + expect(await screen.findByText('about')).toBeInTheDocument(); +}); + +function renderWebApiApp() { + // eslint-disable-next-line testing-library/no-unnecessary-act + renderApp('web-api-v2', ); +} 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 new file mode 100644 index 00000000000..de2113ecb77 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts @@ -0,0 +1,239 @@ +/* + * 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 { dereferenceSchema, mapOpenAPISchema } from '../utils'; + +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', + version: '1.0.0 beta', + }, + paths: { + '/test': { + delete: { + responses: { + '200': { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: { + 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', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + NestedTest: { + type: 'object', + properties: { + test: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + Test: { + type: 'string', + }, + }, + }, + }); +}); + +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', + }, + bool: { + type: 'boolean', + }, + }, + }) + ).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(['GREEN', 'YELLOW', 'RED']); +}); 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 new file mode 100644 index 00000000000..1b5d68380e7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiInformation.tsx @@ -0,0 +1,57 @@ +/* + * 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, SubHeading, Title } from 'design-system'; +import { OpenAPIV3 } from 'openapi-types'; +import React from 'react'; +import { ExcludeReferences } from '../types'; +import { getApiEndpointKey, getMethodClassName } from '../utils'; +import ApiParameters from './ApiParameters'; +import ApiResponses from './ApiResponses'; + +interface Props { + data: ExcludeReferences>; + apiUrl: string; + name: string; + method: string; +} + +export default function ApiInformation({ name, data, method, apiUrl }: Props) { + return ( + <> + {data.summary && {data.summary}} + + + {method} + + {apiUrl.replace(/.*(?=\/api)/, '') + name} + + {data.description &&
{data.description}
} +
+
+ +
+
+ +
+
+ + ); +} 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 new file mode 100644 index 00000000000..d3f82f4a84c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx @@ -0,0 +1,132 @@ +/* + * 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, SubHeading, SubTitle, TextMuted } from 'design-system'; +import { groupBy } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { ExcludeReferences } from '../types'; +import { mapOpenAPISchema } from '../utils'; +import ApiResponseSchema from './ApiResponseSchema'; + +interface Props { + data: ExcludeReferences; +} + +export default function ApiParameters({ data }: Props) { + const [openParameters, setOpenParameters] = React.useState([]); + + const toggleParameter = (name: string) => { + if (openParameters.includes(name)) { + setOpenParameters(openParameters.filter((n) => n !== name)); + } else { + setOpenParameters([...openParameters, name]); + } + }; + + const getSchemaType = (schema: ExcludeReferences) => { + const mappedSchema = mapOpenAPISchema(schema); + + return typeof mappedSchema === 'object' ? JSON.stringify(mappedSchema) : mappedSchema; + }; + + const requestBody = data.requestBody?.content; + + return ( + <> + {translate('api_documentation.v2.parameter_header')} + {!requestBody && !data.parameters?.length && } + {requestBody && ( +
+ + {translate('api_documentation.v2.request_subheader.request_body')} + + +
+ )} + {Object.entries(groupBy(data.parameters, (p) => p.in)).map(([group, parameters]) => ( +
+ + {translate(`api_documentation.v2.request_subheader.${group}`)} + +
    + {parameters.map((parameter) => { + return ( + + {parameter.name}{' '} + {parameter.schema && ( + + )} + {parameter.required && ( + {translate('required')} + )} + {parameter.deprecated && ( + + {translate('deprecated')} + + )} +
+ } + data={parameter.name} + onClick={toggleParameter} + open={openParameters.includes(parameter.name)} + > +
{parameter.description}
+ {parameter.schema?.maximum && ( + + )} + {parameter.schema?.minimum && ( + + )} + {parameter.example !== undefined && ( + + )} + {parameter.schema?.default !== undefined && ( + + )} + + ); + })} + + + ))} + + ); +} 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 new file mode 100644 index 00000000000..bd9ec2db3f9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx @@ -0,0 +1,47 @@ +/* + * 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 { CodeSnippet, TextMuted } from 'design-system'; +import { OpenAPIV3 } from 'openapi-types'; +import React, { HtmlHTMLAttributes } from 'react'; +import { translate } from '../../../helpers/l10n'; +import { ExcludeReferences } from '../types'; +import { mapOpenAPISchema } from '../utils'; + +interface Props extends HtmlHTMLAttributes { + content?: Exclude['content'], undefined>; +} + +export default function ApiResponseSchema(props: Props) { + const { content, ...other } = props; + if (!content || !content['application/json']?.schema) { + return ; + } + + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseTitle.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseTitle.tsx new file mode 100644 index 00000000000..3f58ba830c7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseTitle.tsx @@ -0,0 +1,37 @@ +/* + * 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 { Badge } from 'design-system'; +import React from 'react'; +import { getResponseCodeClassName } from '../utils'; + +interface Props { + code: string; + codeDescription: string; +} + +export default function ApiResponseTitle({ code, codeDescription }: Props) { + return ( +
+ {code} + {codeDescription} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponses.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponses.tsx new file mode 100644 index 00000000000..931f633a7d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponses.tsx @@ -0,0 +1,70 @@ +/* + * 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, SubTitle } from 'design-system'; +import { OpenAPIV3 } from 'openapi-types'; +import React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { ExcludeReferences } from '../types'; +import ApiResponseSchema from './ApiResponseSchema'; +import ApiResponseTitle from './ApiResponseTitle'; + +interface Props { + responses?: ExcludeReferences; +} + +export default function ApiResponses({ responses }: Props) { + const [openedResponses, setOpenedResponses] = React.useState([ + Object.keys(responses ?? {})[0], + ]); + + const toggleParameter = (name: string) => { + if (openedResponses.includes(name)) { + setOpenedResponses(openedResponses.filter((n) => n !== name)); + } else { + setOpenedResponses([...openedResponses, name]); + } + }; + + const titleId = 'api-responses'; + + return ( + <> + {translate('api_documentation.v2.response_header')} + {!responses &&
{translate('no_data')}
} + {responses && ( +
    + {Object.entries(responses).map(([code, response]) => ( + } + data={code} + onClick={toggleParameter} + open={openedResponses.includes(code)} + > + + + ))} +
+ )} + + ); +} 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 new file mode 100644 index 00000000000..8a3cc3e0702 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx @@ -0,0 +1,116 @@ +/* + * 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, InputSearch, Link, SubnavigationAccordion, SubnavigationItem } from 'design-system'; +import { OpenAPIV3 } from 'openapi-types'; +import React, { useMemo, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { translate } from '../../../helpers/l10n'; +import { URL_DIVIDER, getApiEndpointKey, getMethodClassName } from '../utils'; + +interface Api { + name: string; + method: string; + info: OpenAPIV3.OperationObject; +} +interface Props { + docInfo: OpenAPIV3.InfoObject; + apisList: Api[]; +} + +export default function ApiSidebar({ apisList, docInfo }: Props) { + const [search, setSearch] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + const activeApi = location.hash.replace('#', '').split(URL_DIVIDER); + + const handleApiClick = (value: string) => { + navigate(`.#${value}`, { replace: true }); + }; + + const lowerCaseSearch = search.toLowerCase(); + + const groupedList = useMemo( + () => + apisList + .filter( + (api) => + api.name.toLowerCase().includes(lowerCaseSearch) || + api.method.toLowerCase().includes(lowerCaseSearch) || + api.info.summary?.toLowerCase().includes(lowerCaseSearch) + ) + .reduce>((acc, api) => { + const subgroup = api.name.split('/')[1]; + return { + ...acc, + [subgroup]: [...(acc[subgroup] ?? []), api], + }; + }, {}), + [lowerCaseSearch, apisList] + ); + + return ( + <> +

+ + {docInfo.title} + {docInfo.version} + +

+ + + + {Object.entries(groupedList).map(([group, apis]) => ( + name === activeApi[0] && method === activeApi[1] + )} + className="sw-mt-2" + header={group} + key={group} + id={`web-api-${group}`} + > + {apis.map(({ method, name, info }) => ( + +
+ + {method.toUpperCase()} + +
{info.summary ?? name}
+
+ {info.deprecated && {translate('deprecated')}} +
+ ))} +
+ ))} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/queries.ts b/server/sonar-web/src/main/js/apps/web-api-v2/queries.ts new file mode 100644 index 00000000000..1b0bf90d8d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/queries.ts @@ -0,0 +1,26 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { fetchOpenAPI } from '../../api/web-api'; + +export const useOpenAPI = () => { + return useQuery(['open_api'], fetchOpenAPI, { staleTime: Infinity, cacheTime: Infinity }); +}; diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/routes.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/routes.tsx new file mode 100644 index 00000000000..394388bdedd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/routes.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router-dom'; +import WebApiApp from './WebApiApp'; + +const routes = () => } />; + +export default routes; 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 new file mode 100644 index 00000000000..a35f8f868f6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { OpenAPIV3 } from 'openapi-types'; + +export type ExcludeReferences = T extends OpenAPIV3.ReferenceObject + ? never + : T extends object + ? { [K in keyof T]: ExcludeReferences } + : T; + +export type DereferenceRecursive = T extends object + ? T extends OpenAPIV3.ReferenceObject + ? ExcludeReferences + : T extends Array + ? Array> + : { + [K in keyof T]: DereferenceRecursive; + } + : T; 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 new file mode 100644 index 00000000000..768b1b8605b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts @@ -0,0 +1,109 @@ +/* + * 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 { mapValues } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import { DereferenceRecursive, ExcludeReferences } from './types'; + +export const URL_DIVIDER = '-'; + +type ConvertedSchema = string | { [Key: string]: ConvertedSchema } | ConvertedSchema[]; + +export const mapOpenAPISchema = ( + schema: ExcludeReferences +): ConvertedSchema => { + if (schema.type === 'object') { + const result = { ...schema.properties }; + return mapValues(result, (schema) => mapOpenAPISchema(schema)) as ConvertedSchema; + } + if (schema.type === 'array') { + return [mapOpenAPISchema(schema.items)]; + } + if (schema.type === 'string' && schema.enum) { + return schema.enum as ConvertedSchema; + } + if (schema.format) { + return `${schema.type} (${schema.format})`; + } + return schema.type as string; +}; + +export const dereferenceSchema = ( + document: OpenAPIV3.Document +): ExcludeReferences => { + const dereference = (ref: string) => { + const path = ref.replace('#/', '').split('/'); + return path.reduce((acc: any, key) => { + if (key in acc) { + return acc[key]; + } + return {}; + }, document); + }; + + const dereferenceRecursive =

(val: P | OpenAPIV3.ReferenceObject): DereferenceRecursive

=> { + if (typeof val === 'object' && val !== null) { + if ('$ref' in val) { + return dereferenceRecursive(dereference(val.$ref)); + } else if (Array.isArray(val)) { + return val.map(dereferenceRecursive) as DereferenceRecursive

; + } + return mapValues(val, dereferenceRecursive) as DereferenceRecursive

; + } + return val as DereferenceRecursive

; + }; + + return dereferenceRecursive(document) as ExcludeReferences; +}; + +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': + return 'sw-bg-blue-200'; + case '2': + return 'sw-bg-green-200'; + case '3': + return 'sw-bg-yellow-200'; + case '4': + case '5': + return 'sw-bg-red-200'; + default: + return 'sw-bg-gray-200'; + } +}; + +export const getApiEndpointKey = (name: string, method: string) => `${name}${URL_DIVIDER}${method}`; diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 909ed7d284f..50ecf6de2b4 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -4982,6 +4982,7 @@ __metadata: jsdom: 21.1.1 lodash: 4.17.21 lunr: 2.3.9 + openapi-types: 12.1.3 path-browserify: 1.0.1 postcss: 8.4.24 postcss-calc: 9.0.1 @@ -10823,6 +10824,13 @@ __metadata: languageName: node linkType: hard +"openapi-types@npm:12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 7fa5547f87a58d2aa0eba6e91d396f42d7d31bc3ae140e61b5d60b47d2fd068b48776f42407d5a8da7280cf31195aa128c2fc285e8bb871d1105edee5647a0bb + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.2 resolution: "optionator@npm:0.8.2" diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 28b58d317e5..42629e26805 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4,6 +4,7 @@ # #------------------------------------------------------------------------------ +about=About action=Action actions=Actions active=Active @@ -171,6 +172,7 @@ remove=Remove remove_x=Remove {0} rename=Rename replaces=Replaces +required=Required reset_verb=Reset reset_to_default=Reset To Default reset_date=Reset dates @@ -3670,6 +3672,13 @@ api_documentation.parameters=Parameters api_documentation.response_example=Response Example api_documentation.changelog=Changelog api_documentation.search=Search by name... +api_documentation.v2.search=Search API... +api_documentation.v2.parameter_header=Request +api_documentation.v2.response_header=Response +api_documentation.v2.request_subheader.query=Query Parameters +api_documentation.v2.request_subheader.path=Path Parameters +api_documentation.v2.request_subheader.header=Headers +api_documentation.v2.request_subheader.request_body=Request Body #------------------------------------------------------------------------------