mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-30 23:37:28 +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