mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-19 19:30:32 +03:00
feat: add webhooks support (#1304)
This commit is contained in:
parent
171711f79c
commit
41f81b4d96
|
@ -1187,3 +1187,19 @@ components:
|
||||||
shipDate: '2018-10-19T16:46:45Z'
|
shipDate: '2018-10-19T16:46:45Z'
|
||||||
status: placed
|
status: placed
|
||||||
complete: false
|
complete: false
|
||||||
|
x-webhooks:
|
||||||
|
newPet:
|
||||||
|
post:
|
||||||
|
summary: New pet
|
||||||
|
description: Information about a new pet in the systems
|
||||||
|
operationId: newPet
|
||||||
|
tags:
|
||||||
|
- pet
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Pet"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Return a 200 status to indicate that the data was received successfully
|
|
@ -6,7 +6,7 @@ describe('Menu', () => {
|
||||||
it('should have valid items count', () => {
|
it('should have valid items count', () => {
|
||||||
cy.get('.menu-content')
|
cy.get('.menu-content')
|
||||||
.find('li')
|
.find('li')
|
||||||
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8) + 1);
|
.should('have.length', 34);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sync active menu items while scroll', () => {
|
it('should sync active menu items while scroll', () => {
|
||||||
|
|
|
@ -51,10 +51,17 @@ export const ShelfIcon = styled(IntShelfIcon)`
|
||||||
|
|
||||||
export const Badge = styled.span<{ type: string }>`
|
export const Badge = styled.span<{ type: string }>`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 5px;
|
padding: 2px 8px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: ${props => props.theme.colors[props.type].main};
|
background-color: ${props => 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: text-top;
|
vertical-align: middle;
|
||||||
|
line-height: 1.6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
|
||||||
|
font-size: 12px;
|
||||||
|
+ span[type] {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -37,19 +37,22 @@ export class Operation extends React.Component<OperationProps> {
|
||||||
render() {
|
render() {
|
||||||
const { operation } = this.props;
|
const { operation } = this.props;
|
||||||
|
|
||||||
const { name: summary, description, deprecated, externalDocs } = operation;
|
const { name: summary, description, deprecated, externalDocs, isWebhook } = operation;
|
||||||
const hasDescription = !!(description || externalDocs);
|
const hasDescription = !!(description || externalDocs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsContext.Consumer>
|
<OptionsContext.Consumer>
|
||||||
{options => (
|
{(options) => (
|
||||||
<OperationRow>
|
<OperationRow>
|
||||||
<MiddlePanel>
|
<MiddlePanel>
|
||||||
<H2>
|
<H2>
|
||||||
<ShareLink to={operation.id} />
|
<ShareLink to={operation.id} />
|
||||||
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
||||||
|
{isWebhook && <Badge type="primary"> Webhook </Badge>}
|
||||||
</H2>
|
</H2>
|
||||||
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
|
{options.pathInMiddlePanel && !isWebhook && (
|
||||||
|
<Endpoint operation={operation} inverted={true} />
|
||||||
|
)}
|
||||||
{hasDescription && (
|
{hasDescription && (
|
||||||
<Description>
|
<Description>
|
||||||
{description !== undefined && <Markdown source={description} />}
|
{description !== undefined && <Markdown source={description} />}
|
||||||
|
@ -63,7 +66,7 @@ export class Operation extends React.Component<OperationProps> {
|
||||||
<CallbacksList callbacks={operation.callbacks} />
|
<CallbacksList callbacks={operation.callbacks} />
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
<DarkRightPanel>
|
<DarkRightPanel>
|
||||||
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
|
{!options.pathInMiddlePanel && !isWebhook && <Endpoint operation={operation} />}
|
||||||
<RequestSamples operation={operation} />
|
<RequestSamples operation={operation} />
|
||||||
<ResponseSamples operation={operation} />
|
<ResponseSamples operation={operation} />
|
||||||
<CallbackSamples callbacks={operation.callbacks} />
|
<CallbackSamples callbacks={operation.callbacks} />
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { IMenuItem, OperationModel } from '../../services';
|
||||||
import { shortenHTTPVerb } from '../../utils/openapi';
|
import { shortenHTTPVerb } from '../../utils/openapi';
|
||||||
import { MenuItems } from './MenuItems';
|
import { MenuItems } from './MenuItems';
|
||||||
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
|
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
|
||||||
|
import { l } from '../../services/Labels';
|
||||||
|
|
||||||
export interface MenuItemProps {
|
export interface MenuItemProps {
|
||||||
item: IMenuItem;
|
item: IMenuItem;
|
||||||
|
@ -90,7 +91,11 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
|
||||||
deprecated={item.deprecated}
|
deprecated={item.deprecated}
|
||||||
ref={this.ref}
|
ref={this.ref}
|
||||||
>
|
>
|
||||||
|
{item.isWebhook ? (
|
||||||
|
<OperationBadge type="hook">{l('webhook')}</OperationBadge>
|
||||||
|
) : (
|
||||||
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
|
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
|
||||||
|
)}
|
||||||
<MenuItemTitle width="calc(100% - 38px)">
|
<MenuItemTitle width="calc(100% - 38px)">
|
||||||
{item.name}
|
{item.name}
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
|
|
@ -60,6 +60,10 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
|
||||||
&.head {
|
&.head {
|
||||||
background-color: ${props => props.theme.colors.http.head};
|
background-color: ${props => props.theme.colors.http.head};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.hook {
|
||||||
|
background-color: ${props => props.theme.colors.primary.main};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function menuItemActiveBg(depth, { theme }: { theme: ResolvedThemeInterface }): string {
|
function menuItemActiveBg(depth, { theme }: { theme: ResolvedThemeInterface }): string {
|
||||||
|
|
|
@ -8,6 +8,7 @@ export interface LabelsConfig {
|
||||||
nullable: string;
|
nullable: string;
|
||||||
recursive: string;
|
recursive: string;
|
||||||
arrayOf: string;
|
arrayOf: string;
|
||||||
|
webhook: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LabelsConfigRaw = Partial<LabelsConfig>;
|
export type LabelsConfigRaw = Partial<LabelsConfig>;
|
||||||
|
@ -22,6 +23,7 @@ const labels: LabelsConfig = {
|
||||||
nullable: 'Nullable',
|
nullable: 'Nullable',
|
||||||
recursive: 'Recursive',
|
recursive: 'Recursive',
|
||||||
arrayOf: 'Array of ',
|
arrayOf: 'Array of ',
|
||||||
|
webhook: 'Event',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setRedocLabels(_labels?: LabelsConfigRaw) {
|
export function setRedocLabels(_labels?: LabelsConfigRaw) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
OpenAPITag,
|
OpenAPITag,
|
||||||
Referenced,
|
Referenced,
|
||||||
OpenAPIServer,
|
OpenAPIServer,
|
||||||
|
OpenAPIPaths,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
isOperationName,
|
isOperationName,
|
||||||
|
@ -28,6 +29,7 @@ export type ExtendedOpenAPIOperation = {
|
||||||
httpVerb: string;
|
httpVerb: string;
|
||||||
pathParameters: Array<Referenced<OpenAPIParameter>>;
|
pathParameters: Array<Referenced<OpenAPIParameter>>;
|
||||||
pathServers: Array<OpenAPIServer> | undefined;
|
pathServers: Array<OpenAPIServer> | undefined;
|
||||||
|
isWebhook: boolean;
|
||||||
} & OpenAPIOperation;
|
} & OpenAPIOperation;
|
||||||
|
|
||||||
export type TagsInfoMap = Record<string, TagInfo>;
|
export type TagsInfoMap = Record<string, TagInfo>;
|
||||||
|
@ -219,7 +221,12 @@ export class MenuBuilder {
|
||||||
tags[tag.name] = { ...tag, operations: [] };
|
tags[tag.name] = { ...tag, operations: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const paths = spec.paths;
|
getTags(spec.paths);
|
||||||
|
if (spec['x-webhooks']) {
|
||||||
|
getTags(spec['x-webhooks'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTags(paths: OpenAPIPaths, isWebhook?: boolean) {
|
||||||
for (const pathName of Object.keys(paths)) {
|
for (const pathName of Object.keys(paths)) {
|
||||||
const path = paths[pathName];
|
const path = paths[pathName];
|
||||||
const operations = Object.keys(path).filter(isOperationName);
|
const operations = Object.keys(path).filter(isOperationName);
|
||||||
|
@ -251,11 +258,12 @@ export class MenuBuilder {
|
||||||
httpVerb: operationName,
|
httpVerb: operationName,
|
||||||
pathParameters: path.parameters || [],
|
pathParameters: path.parameters || [],
|
||||||
pathServers: path.servers,
|
pathServers: path.servers,
|
||||||
|
isWebhook: !!isWebhook,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
|
||||||
|
|
||||||
import { ContentItemModel, MenuBuilder } from './MenuBuilder';
|
import { ContentItemModel, MenuBuilder } from './MenuBuilder';
|
||||||
import { ApiInfoModel } from './models/ApiInfo';
|
import { ApiInfoModel } from './models/ApiInfo';
|
||||||
|
import { WebhookModel } from './models/Webhook';
|
||||||
import { SecuritySchemesModel } from './models/SecuritySchemes';
|
import { SecuritySchemesModel } from './models/SecuritySchemes';
|
||||||
import { OpenAPIParser } from './OpenAPIParser';
|
import { OpenAPIParser } from './OpenAPIParser';
|
||||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||||
|
@ -15,6 +16,7 @@ export class SpecStore {
|
||||||
externalDocs?: OpenAPIExternalDocumentation;
|
externalDocs?: OpenAPIExternalDocumentation;
|
||||||
contentItems: ContentItemModel[];
|
contentItems: ContentItemModel[];
|
||||||
securitySchemes: SecuritySchemesModel;
|
securitySchemes: SecuritySchemesModel;
|
||||||
|
webhooks?: WebhookModel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
spec: OpenAPISpec,
|
spec: OpenAPISpec,
|
||||||
|
@ -26,5 +28,6 @@ export class SpecStore {
|
||||||
this.externalDocs = this.parser.spec.externalDocs;
|
this.externalDocs = this.parser.spec.externalDocs;
|
||||||
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
|
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
|
||||||
this.securitySchemes = new SecuritySchemesModel(this.parser);
|
this.securitySchemes = new SecuritySchemesModel(this.parser);
|
||||||
|
this.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ export class OperationModel implements IMenuItem {
|
||||||
security: SecurityRequirementModel[];
|
security: SecurityRequirementModel[];
|
||||||
extensions: Record<string, any>;
|
extensions: Record<string, any>;
|
||||||
isCallback: boolean;
|
isCallback: boolean;
|
||||||
|
isWebhook: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private parser: OpenAPIParser,
|
private parser: OpenAPIParser,
|
||||||
|
@ -95,6 +96,7 @@ export class OperationModel implements IMenuItem {
|
||||||
this.operationId = operationSpec.operationId;
|
this.operationId = operationSpec.operationId;
|
||||||
this.path = operationSpec.pathName;
|
this.path = operationSpec.pathName;
|
||||||
this.isCallback = isCallback;
|
this.isCallback = isCallback;
|
||||||
|
this.isWebhook = !!operationSpec.isWebhook;
|
||||||
|
|
||||||
this.name = getOperationSummary(operationSpec);
|
this.name = getOperationSummary(operationSpec);
|
||||||
|
|
||||||
|
|
38
src/services/models/Webhook.ts
Normal file
38
src/services/models/Webhook.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { OpenAPIPath, Referenced } from '../../types';
|
||||||
|
import { OpenAPIParser } from '../OpenAPIParser';
|
||||||
|
import { OperationModel } from './Operation';
|
||||||
|
import { isOperationName } from '../..';
|
||||||
|
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||||
|
|
||||||
|
export class WebhookModel {
|
||||||
|
operations: OperationModel[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
parser: OpenAPIParser,
|
||||||
|
options: RedocNormalizedOptions,
|
||||||
|
infoOrRef?: Referenced<OpenAPIPath>,
|
||||||
|
) {
|
||||||
|
const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {});
|
||||||
|
parser.exitRef(infoOrRef);
|
||||||
|
|
||||||
|
for (const webhookName of Object.keys(webhooks)) {
|
||||||
|
const webhook = webhooks[webhookName];
|
||||||
|
const operations = Object.keys(webhook).filter(isOperationName);
|
||||||
|
for (const operationName of operations) {
|
||||||
|
const operationInfo = webhook[operationName];
|
||||||
|
const operation = new OperationModel(
|
||||||
|
parser,
|
||||||
|
{
|
||||||
|
...operationInfo,
|
||||||
|
httpVerb: operationName,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
options,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.operations.push(operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
src/types/open-api.d.ts
vendored
1
src/types/open-api.d.ts
vendored
|
@ -9,6 +9,7 @@ export interface OpenAPISpec {
|
||||||
security?: OpenAPISecurityRequirement[];
|
security?: OpenAPISecurityRequirement[];
|
||||||
tags?: OpenAPITag[];
|
tags?: OpenAPITag[];
|
||||||
externalDocs?: OpenAPIExternalDocumentation;
|
externalDocs?: OpenAPIExternalDocumentation;
|
||||||
|
'x-webhooks'?: OpenAPIPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAPIInfo {
|
export interface OpenAPIInfo {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user