feat: add webhooks support (#1304)

This commit is contained in:
Anna Stasiuk 2020-08-14 16:33:25 +03:00 committed by GitHub
parent 171711f79c
commit 41f81b4d96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 127 additions and 38 deletions

View File

@ -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

View File

@ -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', () => {

View File

@ -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;
}
`;

View File

@ -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} />

View File

@ -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}

View File

@ -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 {

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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']);
}
}

View File

@ -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);

View 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);
}
}
}
}

View File

@ -9,6 +9,7 @@ export interface OpenAPISpec {
security?: OpenAPISecurityRequirement[];
tags?: OpenAPITag[];
externalDocs?: OpenAPIExternalDocumentation;
'x-webhooks'?: OpenAPIPaths;
}
export interface OpenAPIInfo {