mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-22 00:26:34 +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'
|
||||
status: placed
|
||||
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', () => {
|
||||
cy.get('.menu-content')
|
||||
.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', () => {
|
||||
|
|
|
@ -51,10 +51,17 @@ export const ShelfIcon = styled(IntShelfIcon)`
|
|||
|
||||
export const Badge = styled.span<{ type: string }>`
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
padding: 2px 8px;
|
||||
margin: 0;
|
||||
background-color: ${props => props.theme.colors[props.type].main};
|
||||
color: ${props => props.theme.colors[props.type].contrastText};
|
||||
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() {
|
||||
const { operation } = this.props;
|
||||
|
||||
const { name: summary, description, deprecated, externalDocs } = operation;
|
||||
const { name: summary, description, deprecated, externalDocs, isWebhook } = operation;
|
||||
const hasDescription = !!(description || externalDocs);
|
||||
|
||||
return (
|
||||
<OptionsContext.Consumer>
|
||||
{options => (
|
||||
{(options) => (
|
||||
<OperationRow>
|
||||
<MiddlePanel>
|
||||
<H2>
|
||||
<ShareLink to={operation.id} />
|
||||
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
||||
{isWebhook && <Badge type="primary"> Webhook </Badge>}
|
||||
</H2>
|
||||
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
|
||||
{options.pathInMiddlePanel && !isWebhook && (
|
||||
<Endpoint operation={operation} inverted={true} />
|
||||
)}
|
||||
{hasDescription && (
|
||||
<Description>
|
||||
{description !== undefined && <Markdown source={description} />}
|
||||
|
@ -63,7 +66,7 @@ export class Operation extends React.Component<OperationProps> {
|
|||
<CallbacksList callbacks={operation.callbacks} />
|
||||
</MiddlePanel>
|
||||
<DarkRightPanel>
|
||||
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
|
||||
{!options.pathInMiddlePanel && !isWebhook && <Endpoint operation={operation} />}
|
||||
<RequestSamples operation={operation} />
|
||||
<ResponseSamples operation={operation} />
|
||||
<CallbackSamples callbacks={operation.callbacks} />
|
||||
|
|
|
@ -7,6 +7,7 @@ import { IMenuItem, OperationModel } from '../../services';
|
|||
import { shortenHTTPVerb } from '../../utils/openapi';
|
||||
import { MenuItems } from './MenuItems';
|
||||
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
|
||||
import { l } from '../../services/Labels';
|
||||
|
||||
export interface MenuItemProps {
|
||||
item: IMenuItem;
|
||||
|
@ -90,7 +91,11 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
|
|||
deprecated={item.deprecated}
|
||||
ref={this.ref}
|
||||
>
|
||||
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
|
||||
{item.isWebhook ? (
|
||||
<OperationBadge type="hook">{l('webhook')}</OperationBadge>
|
||||
) : (
|
||||
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
|
||||
)}
|
||||
<MenuItemTitle width="calc(100% - 38px)">
|
||||
{item.name}
|
||||
{this.props.children}
|
||||
|
|
|
@ -60,6 +60,10 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
|
|||
&.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 {
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface LabelsConfig {
|
|||
nullable: string;
|
||||
recursive: string;
|
||||
arrayOf: string;
|
||||
webhook: string;
|
||||
}
|
||||
|
||||
export type LabelsConfigRaw = Partial<LabelsConfig>;
|
||||
|
@ -22,6 +23,7 @@ const labels: LabelsConfig = {
|
|||
nullable: 'Nullable',
|
||||
recursive: 'Recursive',
|
||||
arrayOf: 'Array of ',
|
||||
webhook: 'Event',
|
||||
};
|
||||
|
||||
export function setRedocLabels(_labels?: LabelsConfigRaw) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
OpenAPITag,
|
||||
Referenced,
|
||||
OpenAPIServer,
|
||||
OpenAPIPaths,
|
||||
} from '../types';
|
||||
import {
|
||||
isOperationName,
|
||||
|
@ -28,6 +29,7 @@ export type ExtendedOpenAPIOperation = {
|
|||
httpVerb: string;
|
||||
pathParameters: Array<Referenced<OpenAPIParameter>>;
|
||||
pathServers: Array<OpenAPIServer> | undefined;
|
||||
isWebhook: boolean;
|
||||
} & OpenAPIOperation;
|
||||
|
||||
export type TagsInfoMap = Record<string, TagInfo>;
|
||||
|
@ -219,43 +221,49 @@ export class MenuBuilder {
|
|||
tags[tag.name] = { ...tag, operations: [] };
|
||||
}
|
||||
|
||||
const paths = spec.paths;
|
||||
for (const pathName of Object.keys(paths)) {
|
||||
const path = paths[pathName];
|
||||
const operations = Object.keys(path).filter(isOperationName);
|
||||
for (const operationName of operations) {
|
||||
const operationInfo = path[operationName];
|
||||
let operationTags = operationInfo.tags;
|
||||
getTags(spec.paths);
|
||||
if (spec['x-webhooks']) {
|
||||
getTags(spec['x-webhooks'], true);
|
||||
}
|
||||
|
||||
if (!operationTags || !operationTags.length) {
|
||||
// empty tag
|
||||
operationTags = [''];
|
||||
}
|
||||
function getTags(paths: OpenAPIPaths, isWebhook?: boolean) {
|
||||
for (const pathName of Object.keys(paths)) {
|
||||
const path = paths[pathName];
|
||||
const operations = Object.keys(path).filter(isOperationName);
|
||||
for (const operationName of operations) {
|
||||
const operationInfo = path[operationName];
|
||||
let operationTags = operationInfo.tags;
|
||||
|
||||
for (const tagName of operationTags) {
|
||||
let tag = tags[tagName];
|
||||
if (tag === undefined) {
|
||||
tag = {
|
||||
name: tagName,
|
||||
operations: [],
|
||||
};
|
||||
tags[tagName] = tag;
|
||||
if (!operationTags || !operationTags.length) {
|
||||
// empty tag
|
||||
operationTags = [''];
|
||||
}
|
||||
if (tag['x-traitTag']) {
|
||||
continue;
|
||||
|
||||
for (const tagName of operationTags) {
|
||||
let tag = tags[tagName];
|
||||
if (tag === undefined) {
|
||||
tag = {
|
||||
name: tagName,
|
||||
operations: [],
|
||||
};
|
||||
tags[tagName] = tag;
|
||||
}
|
||||
if (tag['x-traitTag']) {
|
||||
continue;
|
||||
}
|
||||
tag.operations.push({
|
||||
...operationInfo,
|
||||
pathName,
|
||||
pointer: JsonPointer.compile(['paths', pathName, operationName]),
|
||||
httpVerb: operationName,
|
||||
pathParameters: path.parameters || [],
|
||||
pathServers: path.servers,
|
||||
isWebhook: !!isWebhook,
|
||||
});
|
||||
}
|
||||
tag.operations.push({
|
||||
...operationInfo,
|
||||
pathName,
|
||||
pointer: JsonPointer.compile(['paths', pathName, operationName]),
|
||||
httpVerb: operationName,
|
||||
pathParameters: path.parameters || [],
|
||||
pathServers: path.servers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
|
|||
|
||||
import { ContentItemModel, MenuBuilder } from './MenuBuilder';
|
||||
import { ApiInfoModel } from './models/ApiInfo';
|
||||
import { WebhookModel } from './models/Webhook';
|
||||
import { SecuritySchemesModel } from './models/SecuritySchemes';
|
||||
import { OpenAPIParser } from './OpenAPIParser';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
@ -15,6 +16,7 @@ export class SpecStore {
|
|||
externalDocs?: OpenAPIExternalDocumentation;
|
||||
contentItems: ContentItemModel[];
|
||||
securitySchemes: SecuritySchemesModel;
|
||||
webhooks?: WebhookModel;
|
||||
|
||||
constructor(
|
||||
spec: OpenAPISpec,
|
||||
|
@ -26,5 +28,6 @@ export class SpecStore {
|
|||
this.externalDocs = this.parser.spec.externalDocs;
|
||||
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
|
||||
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[];
|
||||
extensions: Record<string, any>;
|
||||
isCallback: boolean;
|
||||
isWebhook: boolean;
|
||||
|
||||
constructor(
|
||||
private parser: OpenAPIParser,
|
||||
|
@ -95,6 +96,7 @@ export class OperationModel implements IMenuItem {
|
|||
this.operationId = operationSpec.operationId;
|
||||
this.path = operationSpec.pathName;
|
||||
this.isCallback = isCallback;
|
||||
this.isWebhook = !!operationSpec.isWebhook;
|
||||
|
||||
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[];
|
||||
tags?: OpenAPITag[];
|
||||
externalDocs?: OpenAPIExternalDocumentation;
|
||||
'x-webhooks'?: OpenAPIPaths;
|
||||
}
|
||||
|
||||
export interface OpenAPIInfo {
|
||||
|
|
Loading…
Reference in New Issue
Block a user