feat: add support x-badges (#2605)

* feat: add support x-badges

Co-authored-by: Max Mueller <maxmueller@eaton.com>

* chore: try to fix e2e tests

* chore: try to fix e2e tests part 2

* Update docs/redoc-vendor-extensions.md

---------

Co-authored-by: Max Mueller <maxmueller@eaton.com>
Co-authored-by: Jacek Łękawa <164185257+JLekawa@users.noreply.github.com>
This commit is contained in:
Alex Varchuk 2024-10-16 11:48:21 +03:00 committed by GitHub
parent 1cceed4b47
commit 64f18779e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 122 additions and 8 deletions

View File

@ -116,6 +116,7 @@ Redoc uses the following [specification extensions](https://redocly.com/docs/api
* [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo * [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo
* [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for tags that refer to non-navigation properties like Pagination, Rate-Limits, etc * [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for tags that refer to non-navigation properties like Pagination, Rate-Limits, etc
* [`x-codeSamples`](docs/redoc-vendor-extensions.md#x-codeSamples) - specify operation code samples * [`x-codeSamples`](docs/redoc-vendor-extensions.md#x-codeSamples) - specify operation code samples
* [`x-badges`](docs/redoc-vendor-extensions.md#x-badges) - specify operation badges
* [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests * [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests
* [`x-nullable`](docs/redoc-vendor-extensions.md#x-nullable) - mark schema param as a nullable * [`x-nullable`](docs/redoc-vendor-extensions.md#x-nullable) - mark schema param as a nullable
* [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories * [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories

View File

@ -22,6 +22,10 @@ paths:
operationId: getMuseumHours operationId: getMuseumHours
tags: tags:
- Operations - Operations
x-badges:
- name: 'Beta'
position: before
color: purple
parameters: parameters:
- $ref: '#/components/parameters/StartDate' - $ref: '#/components/parameters/StartDate'
- $ref: '#/components/parameters/PaginationPage' - $ref: '#/components/parameters/PaginationPage'
@ -64,6 +68,9 @@ paths:
summary: Create special event summary: Create special event
tags: tags:
- Events - Events
x-badges:
- name: 'Alpha'
color: purple
requestBody: requestBody:
required: true required: true
content: content:
@ -92,6 +99,8 @@ paths:
description: Return a list of upcoming special events at the museum. description: Return a list of upcoming special events at the museum.
security: [] security: []
operationId: listSpecialEvents operationId: listSpecialEvents
x-badges:
- name: 'Gamma'
tags: tags:
- Events - Events
parameters: parameters:

View File

@ -106,6 +106,10 @@ paths:
post: post:
tags: tags:
- pet - pet
x-badges:
- name: 'Beta'
position: before
color: purple
summary: Add a new pet to the store summary: Add a new pet to the store
description: Add new pet to the store inventory. description: Add new pet to the store inventory.
operationId: addPet operationId: addPet
@ -150,6 +154,9 @@ paths:
put: put:
tags: tags:
- pet - pet
x-badges:
- name: 'Alpha'
color: purple
summary: Update an existing pet summary: Update an existing pet
description: '' description: ''
operationId: updatePet operationId: updatePet
@ -183,6 +190,8 @@ paths:
get: get:
tags: tags:
- pet - pet
x-badges:
- name: 'Gamma'
summary: Find pet by ID summary: Find pet by ID
description: Returns a single pet description: Returns a single pet
operationId: getPetById operationId: getPetById

View File

@ -252,6 +252,11 @@ lang: JavaScript
source: console.log('Hello World'); source: console.log('Hello World');
``` ```
### x-badges
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-badges | [[Badge Object](https://redocly.com/docs/realm/author/reference/openapi-extensions/x-badges#badge-object)] | A list of badges associated with the operation |
## Parameter Object ## Parameter Object
Extends the OpenAPI [Parameter Object](https://redocly.com/docs/openapi-visual-reference/parameter/) Extends the OpenAPI [Parameter Object](https://redocly.com/docs/openapi-visual-reference/parameter/)

View File

@ -52,6 +52,31 @@ describe('Menu', () => {
cy.location('hash').should('equal', '#schema/Cat'); cy.location('hash').should('equal', '#schema/Cat');
}); });
it('should contains badge schema from x-badges', () => {
cy.contains('h2', 'Add a new pet to the store').scrollIntoView();
cy.contains('h2 > span', 'Beta')
.scrollIntoView()
.wait(100)
.get('[role=menuitem] > label.active')
.children('span[type="badge"]')
.should('have.text', 'Beta');
cy.contains('h2 > span', 'Alpha')
.scrollIntoView()
.wait(100)
.get('[role=menuitem] > label.active')
.children('span[type="badge"]')
.should('have.text', 'Alpha');
cy.contains('h2 > span', 'Gamma')
.scrollIntoView()
.wait(100)
.get('[role=menuitem] > label.active')
.children('span[type="badge"]')
.should('have.text', 'Gamma');
});
it('should contains Cat schema in Pet using x-tags', () => { it('should contains Cat schema in Pet using x-tags', () => {
cy.contains('[role=menuitem] > label.-depth1', 'pet').click({ force: true }); cy.contains('[role=menuitem] > label.-depth1', 'pet').click({ force: true });
cy.location('hash').should('equal', '#tag/pet'); cy.location('hash').should('equal', '#tag/pet');

View File

@ -47,11 +47,11 @@ export const ShelfIcon = styled(IntShelfIcon)`
} }
`; `;
export const Badge = styled.span<{ type: string }>` export const Badge = styled.span<{ type: string; color?: string }>`
display: inline-block; display: inline-block;
padding: 2px 8px; padding: 2px 8px;
margin: 0; margin: 0;
background-color: ${props => props.theme.colors[props.type].main}; background-color: ${props => props.color || props.theme.colors[props.type].main};
color: ${props => props.theme.colors[props.type].contrastText}; color: ${props => props.theme.colors[props.type].contrastText};
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${props => props.theme.typography.code.fontSize};
vertical-align: middle; vertical-align: middle;

View File

@ -28,9 +28,20 @@ export interface OperationProps {
} }
export const Operation = observer(({ operation }: OperationProps): JSX.Element => { export const Operation = observer(({ operation }: OperationProps): JSX.Element => {
const { name: summary, description, deprecated, externalDocs, isWebhook, httpVerb } = operation; const {
name: summary,
description,
deprecated,
externalDocs,
isWebhook,
httpVerb,
badges,
} = operation;
const hasDescription = !!(description || externalDocs); const hasDescription = !!(description || externalDocs);
const { showWebhookVerb } = React.useContext(OptionsContext); const { showWebhookVerb } = React.useContext(OptionsContext);
const badgesBefore = badges.filter(({ position }) => position === 'before');
const badgesAfter = badges.filter(({ position }) => position === 'after');
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => (
@ -38,6 +49,11 @@ export const Operation = observer(({ operation }: OperationProps): JSX.Element =
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink to={operation.id} /> <ShareLink to={operation.id} />
{badgesBefore.map(({ name, color }) => (
<Badge type="primary" key={name} color={color}>
{name}
</Badge>
))}
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
{isWebhook && ( {isWebhook && (
<Badge type="primary"> <Badge type="primary">
@ -45,6 +61,11 @@ export const Operation = observer(({ operation }: OperationProps): JSX.Element =
Webhook {showWebhookVerb && httpVerb && '| ' + httpVerb.toUpperCase()} Webhook {showWebhookVerb && httpVerb && '| ' + httpVerb.toUpperCase()}
</Badge> </Badge>
)} )}
{badgesAfter.map(({ name, color }) => (
<Badge type="primary" key={name} color={color}>
{name}
</Badge>
))}
</H2> </H2>
{options.pathInMiddlePanel && !isWebhook && ( {options.pathInMiddlePanel && !isWebhook && (
<Endpoint operation={operation} inverted={true} /> <Endpoint operation={operation} inverted={true} />

View File

@ -101,6 +101,12 @@ export const OperationMenuItemContent = observer((props: OperationMenuItemConten
$deprecated={item.deprecated} $deprecated={item.deprecated}
ref={ref} ref={ref}
> >
{item.badges &&
item.badges?.map(({ name, color }) => (
<OperationBadge type="badge" color={color} key={name}>
{name}
</OperationBadge>
))}
{item.isWebhook ? ( {item.isWebhook ? (
<OperationBadge type="hook"> <OperationBadge type="hook">
{showWebhookVerb ? item.httpVerb : l('webhook')} {showWebhookVerb ? item.httpVerb : l('webhook')}

View File

@ -4,14 +4,14 @@ import { darken } from 'polished';
import { deprecatedCss, ShelfIcon } from '../../common-elements'; import { deprecatedCss, ShelfIcon } from '../../common-elements';
import styled, { css, media, ResolvedThemeInterface } from '../../styled-components'; import styled, { css, media, ResolvedThemeInterface } from '../../styled-components';
export const OperationBadge = styled.span.attrs((props: { type: string }) => ({ export const OperationBadge = styled.span.attrs((props: { type: string; color?: string }) => ({
className: `operation-type ${props.type}`, className: `operation-type ${props.type}`,
}))<{ type: string }>` }))<{ type: string; color?: string }>`
width: 9ex; width: 9ex;
display: inline-block; display: inline-block;
height: ${props => props.theme.typography.code.fontSize}; height: ${props => props.theme.typography.code.fontSize};
line-height: ${props => props.theme.typography.code.fontSize}; line-height: ${props => props.theme.typography.code.fontSize};
background-color: #333; background-color: ${props => props.color || '#333'};
border-radius: 3px; border-radius: 3px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 6px 4px; background-position: 6px 4px;

View File

@ -20,7 +20,12 @@ import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response'; import { ResponseModel } from './Response';
import { SideNavStyleEnum } from '../types'; import { SideNavStyleEnum } from '../types';
import type { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types'; import type {
OpenAPIExternalDocumentation,
OpenAPIServer,
OpenAPIXBadges,
OpenAPIXCodeSample,
} from '../../types';
import type { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import type { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import type { MediaContentModel } from './MediaContent'; import type { MediaContentModel } from './MediaContent';
@ -71,6 +76,7 @@ export class OperationModel implements IMenuItem {
operationId?: string; operationId?: string;
operationHash?: string; operationHash?: string;
httpVerb: string; httpVerb: string;
badges: OpenAPIXBadges[];
deprecated: boolean; deprecated: boolean;
path: string; path: string;
servers: OpenAPIServer[]; servers: OpenAPIServer[];
@ -112,6 +118,12 @@ export class OperationModel implements IMenuItem {
: options.sideNavStyle === SideNavStyleEnum.PathOnly : options.sideNavStyle === SideNavStyleEnum.PathOnly
? this.path ? this.path
: this.name; : this.name;
this.badges =
operationSpec['x-badges']?.map(({ name, color, position }) => ({
name,
color: color,
position: position || 'after',
})) || [];
if (this.isCallback) { if (this.isCallback) {
// NOTE: Callbacks by default should not inherit the specification's global `security` definition. // NOTE: Callbacks by default should not inherit the specification's global `security` definition.

View File

@ -70,6 +70,12 @@ export interface OpenAPIXCodeSample {
source: string; source: string;
} }
export interface OpenAPIXBadges {
name: string;
color?: string;
position?: 'before' | 'after';
}
export interface OpenAPIOperation { export interface OpenAPIOperation {
tags?: string[]; tags?: string[];
summary?: string; summary?: string;
@ -85,6 +91,7 @@ export interface OpenAPIOperation {
servers?: OpenAPIServer[]; servers?: OpenAPIServer[];
'x-codeSamples'?: OpenAPIXCodeSample[]; 'x-codeSamples'?: OpenAPIXCodeSample[];
'x-code-samples'?: OpenAPIXCodeSample[]; // deprecated 'x-code-samples'?: OpenAPIXCodeSample[]; // deprecated
'x-badges'?: OpenAPIXBadges[];
} }
export interface OpenAPIParameter { export interface OpenAPIParameter {

View File

@ -581,6 +581,13 @@ and standard method from web, mobile and desktop applications.
"tags": [ "tags": [
"pet", "pet",
], ],
"x-badges": [
{
"color": "purple",
"name": "Beta",
"position": "before",
},
],
"x-codeSamples": [ "x-codeSamples": [
{ {
"lang": "C#", "lang": "C#",
@ -645,6 +652,12 @@ try {
"tags": [ "tags": [
"pet", "pet",
], ],
"x-badges": [
{
"color": "purple",
"name": "Alpha",
},
],
"x-codeSamples": [ "x-codeSamples": [
{ {
"lang": "PHP", "lang": "PHP",
@ -883,6 +896,11 @@ try {
"tags": [ "tags": [
"pet", "pet",
], ],
"x-badges": [
{
"name": "Gamma",
},
],
}, },
"post": { "post": {
"description": "", "description": "",

View File

@ -660,6 +660,7 @@ export function isRedocExtension(key: string): boolean {
'x-servers': true, 'x-servers': true,
'x-tagGroups': true, 'x-tagGroups': true,
'x-traitTag': true, 'x-traitTag': true,
'x-badges': true,
'x-additionalPropertiesName': true, 'x-additionalPropertiesName': true,
'x-explicitMappingOnly': true, 'x-explicitMappingOnly': true,
}; };