feat: add callbacks support (#1224)

Co-authored-by: Jonathan Bailey <jonathan_bailey@bose.com>
Co-authored-by: Roman Hotsiy <gotsijroman@gmail.com>
This commit is contained in:
Oleksiy Kachynskyy 2020-04-08 14:04:58 +03:00 committed by GitHub
parent 5bd2e6227b
commit 57e93ec435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1229 additions and 133 deletions

View File

@ -50,11 +50,13 @@ $ npm run unit
# run e2e tests
$ npm run e2e
# Make sure you have created bundle before running e2e test
# E.g. run `npm run bundle` and wait for the finishing process.
# open cypress UI to debug e2e test
$ npm run cy:open
# run the full test suite, include linting / unit / e2e
# run the unit tests (includes linting and license checks)
$ npm test
# prepare bundles

View File

@ -489,6 +489,234 @@ paths:
description: Invalid ID supplied
'404':
description: Order not found
/store/subscribe:
post:
tags:
- store
summary: Subscribe to the Store events
description: Add subscription for a store events
requestBody:
content:
application/json:
schema:
type: object
properties:
callbackUrl:
type: string
format: uri
description: This URL will be called by the server when the desired event will occur
example: https://myserver.com/send/callback/here
eventName:
type: string
description: Event name for the subscription
enum:
- orderInProgress
- orderShipped
- orderDelivered
example: orderInProgress
required:
- callbackUrl
- eventName
responses:
'201':
description: Subscription added
content:
application/json:
schema:
type: object
properties:
subscriptionId:
type: string
example: AAA-123-BBB-456
callbacks:
orderInProgress:
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
servers:
- url: //callback-url.path-level/v1
description: Path level server 1
- url: //callback-url.path-level/v2
description: Path level server 2
post:
summary: Order in Progress (Summary)
description: A callback triggered every time an Order is updated status to "inProgress" (Description)
externalDocs:
description: Find out more
url: 'https://more-details.com/demo'
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
status:
type: string
example: 'inProgress'
application/xml:
schema:
type: object
properties:
orderId:
type: string
example: '123'
example: |
<?xml version="1.0" encoding="UTF-8"?>
<root>
<orderId>123</orderId>
<status>inProgress</status>
<timestamp>2018-10-19T16:46:45Z</timestamp>
</root>
responses:
'200':
description: Callback successfully processed and no retries will be performed
content:
application/json:
schema:
type: object
properties:
someProp:
type: string
example: '123'
'299':
description: Response for cancelling subscription
'500':
description: Callback processing failed and retries will be performed
x-code-samples:
- lang: 'C#'
source: |
PetStore.v1.Pet pet = new PetStore.v1.Pet();
pet.setApiKey("your api key");
pet.petType = PetStore.v1.Pet.TYPE_DOG;
pet.name = "Rex";
// set other fields
PetStoreResponse response = pet.create();
if (response.statusCode == HttpStatusCode.Created)
{
// Successfully created
}
else
{
// Something wrong -- check response for errors
Console.WriteLine(response.getRawResponse());
}
- lang: PHP
source: |
$form = new \PetStore\Entities\Pet();
$form->setPetType("Dog");
$form->setName("Rex");
// set other fields
try {
$pet = $client->pets()->create($form);
} catch (UnprocessableEntityException $e) {
var_dump($e->getErrors());
}
put:
description: Order in Progress (Only Description)
servers:
- url: //callback-url.operation-level/v1
description: Operation level server 1 (Operation override)
- url: //callback-url.operation-level/v2
description: Operation level server 2 (Operation override)
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
status:
type: string
example: 'inProgress'
application/xml:
schema:
type: object
properties:
orderId:
type: string
example: '123'
example: |
<?xml version="1.0" encoding="UTF-8"?>
<root>
<orderId>123</orderId>
<status>inProgress</status>
<timestamp>2018-10-19T16:46:45Z</timestamp>
</root>
responses:
'200':
description: Callback successfully processed and no retries will be performed
content:
application/json:
schema:
type: object
properties:
someProp:
type: string
example: '123'
orderShipped:
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
post:
description: |
Very long description
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
estimatedDeliveryDate:
type: string
format: date-time
example: '2018-11-11T16:00:00Z'
responses:
'200':
description: Callback successfully processed and no retries will be performed
orderDelivered:
'http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}':
post:
deprecated: true
summary: Order delivered
description: A callback triggered every time an Order is delivered to the recipient
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
responses:
'200':
description: Callback successfully processed and no retries will be performed
/user:
post:
tags:
@ -955,7 +1183,7 @@ components:
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
quantity: 1
shipDate: '2018-10-19T16:46:45Z'
status: placed
complete: false

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));
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8) + 1);
});
it('should sync active menu items while scroll', () => {

243
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "redoc",
"version": "2.0.0-rc.24",
"version": "2.0.0-rc.26",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2867,14 +2867,26 @@
}
},
"bl": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.1.tgz",
"integrity": "sha512-FL/TdvchukRCuWVxT0YMO/7+L5TNeNrVFvRU2IY63aUyv9mpt8splf2NEr6qXtPo5fya5a66YohQKvGNmLrWNA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
"integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"buffer": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
"integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
"dev": true,
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@ -7270,22 +7282,182 @@
"dev": true
},
"handlebars": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz",
"integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.4.tgz",
"integrity": "sha512-Is8+SzHv8K9STNadlBVpVhxXrSXxVgTyIvhdg2Qjak1SfSZ7iEozLHdwiX1jJ9lLFkcFJxqGK5s/cI7ZX+qGkQ==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
"optimist": "^0.6.1",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4"
"uglify-js": "^3.1.4",
"yargs": "^15.3.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"yargs": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
"integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
"dev": true,
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.1"
}
},
"yargs-parser": {
"version": "18.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
"integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
@ -9444,8 +9616,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"resolved": "",
"dev": true,
"optional": true
}
@ -11485,6 +11656,12 @@
}
}
},
"mkdirp-classic": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz",
"integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==",
"dev": true
},
"mobx": {
"version": "4.15.4",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-4.15.4.tgz",
@ -12098,24 +12275,6 @@
"is-wsl": "^1.1.0"
}
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"dev": true,
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
},
"dependencies": {
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
"dev": true
}
}
},
"optionator": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
@ -14939,13 +15098,13 @@
"dev": true
},
"tar-fs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz",
"integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp": "^0.5.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
@ -15412,9 +15571,9 @@
"dev": true
},
"uglify-js": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz",
"integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==",
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.1.tgz",
"integrity": "sha512-W7KxyzeaQmZvUFbGj4+YFshhVrMBGSg2IbcYAjGWGvx8DHvJMclbTDMpffdxFUGPBHjIytk7KJUR/KUXstUGDw==",
"dev": true,
"optional": true,
"requires": {
@ -16308,8 +16467,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"resolved": "",
"dev": true,
"optional": true
}
@ -17407,8 +17565,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"resolved": "",
"dev": true,
"optional": true
}
@ -17776,12 +17933,6 @@
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
"dev": true
},
"worker-farm": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import styled from '../../styled-components';
import { DropdownProps } from '../../common-elements';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { OperationModel } from '../../services/models';
import { XPayloadSample } from '../../services/models/Operation';
import { isPayloadSample } from '../../services';
export interface PayloadSampleProps {
callback: OperationModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
}
export class CallbackPayloadSample extends React.Component<PayloadSampleProps> {
render() {
const payloadSample = this.props.callback.codeSamples.find(sample =>
isPayloadSample(sample),
) as XPayloadSample | undefined;
if (!payloadSample) {
return null;
}
return (
<PayloadSampleWrapper>
<PayloadSamples content={payloadSample.requestBodyContent} />
</PayloadSampleWrapper>
);
}
}
export const PayloadSampleWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -0,0 +1,79 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import styled from '../../styled-components';
import { RightPanelHeader } from '../../common-elements';
import { RedocNormalizedOptions } from '../../services';
import { CallbackModel } from '../../services/models';
import { OptionsContext } from '../OptionsProvider';
import { GenericChildrenSwitcher } from '../GenericChildrenSwitcher/GenericChildrenSwitcher';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { InvertedSimpleDropdown, MimeLabel } from '../PayloadSamples/styled.elements';
import { CallbackPayloadSample } from './CallbackReqSamples';
export interface CallbackSamplesProps {
callbacks: CallbackModel[];
}
@observer
export class CallbackSamples extends React.Component<CallbackSamplesProps> {
static contextType = OptionsContext;
context: RedocNormalizedOptions;
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
render() {
const { callbacks } = this.props;
if (!callbacks || callbacks.length === 0) {
return null;
}
const operations = callbacks
.map(callback => callback.operations.map(operation => operation))
.reduce((a, b) => a.concat(b), []);
const hasSamples = operations.some(operation => operation.codeSamples.length > 0);
if (!hasSamples) {
return null;
}
const dropdownOptions = operations.map((callback, idx) => {
return {
label: `${callback.httpVerb.toUpperCase()}: ${callback.name}`,
value: idx.toString(),
};
});
return (
<div>
<RightPanelHeader> Callback payload samples </RightPanelHeader>
<SamplesWrapper>
<GenericChildrenSwitcher
items={operations}
renderDropdown={this.renderDropdown}
label={'Callback'}
options={dropdownOptions}
>
{callback => (
<CallbackPayloadSample
key="callbackPayloadSample"
callback={callback}
renderDropdown={this.renderDropdown}
/>
)}
</GenericChildrenSwitcher>
</SamplesWrapper>
</div>
);
}
}
export const SamplesWrapper = styled.div`
background: ${({ theme }) => theme.codeBlock.backgroundColor};
padding: ${props => props.theme.spacing.unit * 4}px;
`;

View File

@ -0,0 +1,46 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { OperationModel } from '../../services/models';
import styled from '../../styled-components';
import { Endpoint } from '../Endpoint/Endpoint';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Extensions } from '../Fields/Extensions';
import { Markdown } from '../Markdown/Markdown';
import { Parameters } from '../Parameters/Parameters';
import { ResponsesList } from '../Responses/ResponsesList';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { CallbackDetailsWrap } from './styled.elements';
export interface CallbackDetailsProps {
operation: OperationModel;
}
@observer
export class CallbackDetails extends React.Component<CallbackDetailsProps> {
render() {
const { operation } = this.props;
const { description, externalDocs } = operation;
const hasDescription = !!(description || externalDocs);
return (
<CallbackDetailsWrap>
{hasDescription && (
<Description>
{description !== undefined && <Markdown source={description} />}
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</Description>
)}
<Endpoint operation={this.props.operation} inverted={true} compact={true} />
<Extensions extensions={operation.extensions} />
<SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} isCallback={operation.isCallback} />
</CallbackDetailsWrap>
);
}
}
const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 3}px;
`;

View File

@ -0,0 +1,30 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { OperationModel } from '../../services/models';
import { StyledCallbackTitle } from './styled.elements';
import { CallbackDetails } from './CallbackDetails';
@observer
export class CallbackOperation extends React.Component<{ callbackOperation: OperationModel }> {
toggle = () => {
this.props.callbackOperation.toggle();
};
render() {
const { name, expanded, httpVerb, deprecated } = this.props.callbackOperation;
return (
<>
<StyledCallbackTitle
onClick={this.toggle}
name={name}
opened={expanded}
httpVerb={httpVerb}
deprecated={deprecated}
/>
{expanded && <CallbackDetails operation={this.props.callbackOperation} />}
</>
);
}
}

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import { darken } from 'polished';
import { ShelfIcon } from '../../common-elements';
import { OperationBadge } from '../SideMenu/styled.elements';
import { shortenHTTPVerb } from '../../utils/openapi';
import styled from '../../styled-components';
import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
export interface CallbackTitleProps {
name: string;
opened?: boolean;
httpVerb: string;
deprecated?: boolean;
className?: string;
onClick?: () => void;
}
export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
render() {
const { name, opened, className, onClick, httpVerb, deprecated } = this.props;
return (
<CallbackTitleWrapper className={className} onClick={onClick || undefined}>
<OperationBadgeStyled type={httpVerb}>{shortenHTTPVerb(httpVerb)}</OperationBadgeStyled>
<ShelfIcon size={'1.5em'} direction={opened ? 'down' : 'right'} float={'left'} />
<CallbackName deprecated={deprecated}>{name}</CallbackName>
{deprecated ? <Badge type="warning"> {l('deprecated')} </Badge> : null}
</CallbackTitleWrapper>
);
}
}
const CallbackTitleWrapper = styled.div`
& > * {
vertical-align: middle;
}
${ShelfIcon} {
polygon {
fill: ${({ theme }) => darken(theme.colors.tonalOffset, theme.colors.gray[100])};
}
}
`;
const CallbackName = styled.span<{ deprecated?: boolean }>`
text-decoration: ${props => (props.deprecated ? 'line-through' : 'none')};
margin-right: 8px;
`;
const OperationBadgeStyled = styled(OperationBadge)`
margin: 0px 5px 0px 0px;
`;

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import { CallbackModel } from '../../services/models';
import styled from '../../styled-components';
import { CallbackOperation } from './CallbackOperation';
export interface CallbacksListProps {
callbacks: CallbackModel[];
}
export class CallbacksList extends React.PureComponent<CallbacksListProps> {
render() {
const { callbacks } = this.props;
if (!callbacks || callbacks.length === 0) {
return null;
}
return (
<div>
<CallbacksHeader> Callbacks </CallbacksHeader>
{callbacks.map(callback => {
return callback.operations.map((operation, index) => {
return (
<CallbackOperation key={`${callback.name}_${index}`} callbackOperation={operation} />
);
});
})}
</div>
);
}
}
const CallbacksHeader = styled.h3`
font-size: 1.3em;
padding: 0.2em 0;
margin: 3em 0 1.1em;
color: ${({ theme }) => theme.colors.text.primary};
font-weight: normal;
`;

View File

@ -0,0 +1,3 @@
export * from './CallbackOperation';
export * from './CallbackTitle';
export * from './CallbacksList';

View File

@ -0,0 +1,18 @@
import styled from '../../styled-components';
import { CallbackTitle } from './CallbackTitle';
export const StyledCallbackTitle = styled(CallbackTitle)`
padding: 10px;
border-radius: 2px;
margin-bottom: 4px;
line-height: 1.5em;
background-color: ${({ theme }) => theme.colors.gray[100]};
cursor: pointer;
`;
export const CallbackDetailsWrap = styled.div`
padding: 10px 25px;
background-color: ${({ theme }) => theme.colors.gray[50]};
margin-bottom: 5px;
margin-top: 5px;
`;

View File

@ -3,7 +3,6 @@ import * as React from 'react';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { ContentItemModel } from '../../services/MenuBuilder';
import { GroupModel, OperationModel } from '../../services/models';
@ -18,7 +17,9 @@ export class ContentItems extends React.Component<{
if (items.length === 0) {
return null;
}
return items.map(item => <ContentItem item={item} key={item.id} />);
return items.map(item => {
return <ContentItem key={item.id} item={item} />;
});
}
}

View File

@ -21,6 +21,7 @@ export interface EndpointProps {
hideHostname?: boolean;
inverted?: boolean;
compact?: boolean;
}
export interface EndpointState {
@ -49,7 +50,9 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
{options => (
<OperationEndpointWrap>
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
<HttpVerb type={operation.httpVerb} compact={this.props.compact}>
{operation.httpVerb}
</HttpVerb>
<ServerRelativeURL>{operation.path}</ServerRelativeURL>
<ShelfIcon
float={'right'}

View File

@ -34,14 +34,14 @@ export const EndpointInfo = styled.div<{ expanded?: boolean; inverted?: boolean
}
`;
export const HttpVerb = styled.span.attrs((props: { type: string }) => ({
export const HttpVerb = styled.span.attrs((props: { type: string; compact?: boolean }) => ({
className: `http-verb ${props.type}`,
}))<{ type: string }>`
font-size: 0.929em;
line-height: 20px;
background-color: ${(props: any) => props.theme.colors.http[props.type] || '#999999'};
}))<{ type: string; compact?: boolean }>`
font-size: ${props => (props.compact ? '0.8em' : '0.929em')};
line-height: ${props => (props.compact ? '18px' : '20px')};
background-color: ${props => props.theme.colors.http[props.type] || '#999999'};
color: #ffffff;
padding: 3px 10px;
padding: ${props => (props.compact ? '2px 8px' : '3px 10px')};
text-transform: uppercase;
font-family: ${props => props.theme.typography.headings.fontFamily};
margin: 0;
@ -59,7 +59,6 @@ export const ServersOverlay = styled.div<{ expanded: boolean }>`
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
transition: all 0.25s ease;
${props => (props.expanded ? '' : 'transform: translateY(-50%) scaleY(0);')}
`;

View File

@ -0,0 +1,74 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { DropdownProps, DropdownOption } from '../../common-elements/dropdown';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface GenericChildrenSwitcherProps<T> {
items?: T[];
options: DropdownOption[];
label?: string;
renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeItem: T) => JSX.Element;
}
export interface GenericChildrenSwitcherState {
activeItemIdx: number;
}
/**
* TODO: Refactor this component:
* Implement rendering dropdown/label directly in this component
* Accept as a parameter mapper-function for building dropdown option labels
*/
@observer
export class GenericChildrenSwitcher<T> extends React.Component<
GenericChildrenSwitcherProps<T>,
GenericChildrenSwitcherState
> {
constructor(props) {
super(props);
this.state = {
activeItemIdx: 0,
};
}
switchItem = ({ value }) => {
if (this.props.items) {
this.setState({
activeItemIdx: parseInt(value, 10),
});
}
};
render() {
const { items } = this.props;
if (!items || !items.length) {
return null;
}
const Wrapper = ({ children }) =>
this.props.label ? (
<DropdownWrapper>
<DropdownLabel>{this.props.label}</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return (
<>
<Wrapper>
{this.props.renderDropdown({
value: this.props.options[this.state.activeItemIdx],
options: this.props.options,
onChange: this.switchItem,
})}
</Wrapper>
{this.props.children(items[this.state.activeItemIdx])}
</>
);
}
}

View File

@ -1,29 +1,26 @@
import * as React from 'react';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';
import { OptionsContext } from '../OptionsProvider';
import { ShareLink } from '../../common-elements/linkify';
import { OperationModel } from '../../services/models';
import styled from '../../styled-components';
import { CallbacksList } from '../Callbacks';
import { CallbackSamples } from '../CallbackSamples/CallbackSamples';
import { Endpoint } from '../Endpoint/Endpoint';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Extensions } from '../Fields/Extensions';
import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider';
import { Parameters } from '../Parameters/Parameters';
import { RequestSamples } from '../RequestSamples/RequestSamples';
import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components';
import { Extensions } from '../Fields/Extensions';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
const OperationRow = styled(Row)`
backface-visibility: hidden;
contain: content;
overflow: hidden;
`;
@ -32,7 +29,7 @@ const Description = styled.div`
`;
export interface OperationProps {
operation: OperationType;
operation: OperationModel;
}
@observer
@ -63,11 +60,13 @@ export class Operation extends React.Component<OperationProps> {
<SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} />
<CallbacksList callbacks={operation.callbacks} />
</MiddlePanel>
<DarkRightPanel>
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
<RequestSamples operation={operation} />
<ResponseSamples operation={operation} />
<CallbackSamples callbacks={operation.callbacks} />
</DarkRightPanel>
</OperationRow>
)}

View File

@ -4,14 +4,16 @@ import ReactDropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div`
padding: 12px;
padding: 0.9em;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0;
display: block;
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 0.929em;
line-height: 1.5em;
`;
export const DropdownLabel = styled.span`
@ -36,6 +38,11 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-placeholder {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.Dropdown-control {
margin-top: 0;
}
@ -55,6 +62,11 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
.Dropdown-menu {
margin: 0;
margin-top: 2px;
.Dropdown-option {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
`;

View File

@ -4,20 +4,21 @@ import styled from '../../styled-components';
import { ResponseView } from './Response';
const ResponsesHeader = styled.h3`
font-size: 18px;
font-size: 1.3em;
padding: 0.2em 0;
margin: 3em 0 1.1em;
color: #253137;
color: ${({ theme }) => theme.colors.text.primary};
font-weight: normal;
`;
export interface ResponseListProps {
responses: ResponseModel[];
isCallback?: boolean;
}
export class ResponsesList extends React.PureComponent<ResponseListProps> {
render() {
const { responses } = this.props;
const { responses, isCallback } = this.props;
if (!responses || responses.length === 0) {
return null;
@ -25,7 +26,7 @@ export class ResponsesList extends React.PureComponent<ResponseListProps> {
return (
<div>
<ResponsesHeader> Responses </ResponsesHeader>
<ResponsesHeader>{isCallback ? 'Callback responses' : 'Responses'}</ResponsesHeader>
{responses.map(response => {
return <ResponseView key={response.code} response={response} />;
})}

View File

@ -0,0 +1,59 @@
/* tslint:disable:no-implicit-dependencies */
import { shallow } from 'enzyme';
import * as React from 'react';
import { OpenAPIParser } from '../../services';
import { CallbackModel } from '../../services/models/Callback';
import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
import { CallbacksList, CallbackTitle, CallbackOperation } from '../Callbacks';
import * as simpleCallbackFixture from './fixtures/simple-callback.json';
const options = new RedocNormalizedOptions({});
describe('Components', () => {
describe('Callbacks', () => {
it('should correctly render CallbackView', () => {
const parser = new OpenAPIParser(simpleCallbackFixture, undefined, options);
const callback = new CallbackModel(
parser,
'Test.Callback',
{ $ref: '#/components/callbacks/Test' },
'',
options,
);
// There should be 1 operation defined in simple-callback.json, just get it manually for readability.
const callbackViewElement = shallow(
<CallbackOperation key={callback.name} callbackOperation={callback.operations[0]} />,
).getElement();
expect(callbackViewElement.props).toBeDefined();
expect(callbackViewElement.props.children).toBeDefined();
expect(callbackViewElement.props.children.length).toBeGreaterThan(0);
});
it('should correctly render CallbackTitle', () => {
const callbackTitleViewElement = shallow(
<CallbackTitle name={'Test'} className={'.test'} onClick={undefined} httpVerb={'get'} />,
).getElement();
expect(callbackTitleViewElement.props).toBeDefined();
expect(callbackTitleViewElement.props.className).toEqual('.test');
expect(callbackTitleViewElement.props.onClick).toBeUndefined();
});
it('should correctly render CallbacksList', () => {
const parser = new OpenAPIParser(simpleCallbackFixture, undefined, options);
const callback = new CallbackModel(
parser,
'Test.Callback',
{ $ref: '#/components/callbacks/Test' },
'',
options,
);
const callbacksListViewElement = shallow(
<CallbacksList callbacks={[callback]} />,
).getElement();
expect(callbacksListViewElement.props).toBeDefined();
expect(callbacksListViewElement.props.children).toBeDefined();
expect(callbacksListViewElement.props.children.length).toBeGreaterThan(0);
});
});
});

View File

@ -0,0 +1,66 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "Foo"
},
"components": {
"callbacks": {
"Test": {
"/test": {
"post": {
"operationId": "testCallback",
"description": "Test callback.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "TestTitle",
"type": "object",
"description": "Test description",
"properties": {
"type": {
"type": "string",
"description": "The type of response.",
"enum": [
"TestResponse.Complete"
]
},
"status": {
"type": "string",
"enum": [
"FAILURE",
"SUCCESS"
]
}
},
"required": [
"status"
]
}
}
}
},
"parameters": [
{
"name": "X-Test-Header",
"in": "header",
"required": true,
"example": "1",
"description": "This is a test header parameter",
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Test response."
}
}
}
}
}
}
}
}

View File

@ -1,8 +1,16 @@
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
import {
OpenAPIOperation,
OpenAPIParameter,
OpenAPISpec,
OpenAPITag,
Referenced,
OpenAPIServer,
} from '../types';
import {
isOperationName,
SECURITY_DEFINITIONS_COMPONENT_NAME,
setSecuritySchemePrefix,
JsonPointer,
} from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models';
@ -15,9 +23,11 @@ export type TagInfo = OpenAPITag & {
};
export type ExtendedOpenAPIOperation = {
pointer: string;
pathName: string;
httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
} & OpenAPIOperation;
export type TagsInfoMap = Dict<TagInfo>;
@ -237,8 +247,10 @@ export class MenuBuilder {
tag.operations.push({
...operationInfo,
pathName,
pointer: JsonPointer.compile(['paths', pathName, operationName]),
httpVerb: operationName,
pathParameters: path.parameters || [],
pathServers: path.servers,
});
}
}

View File

@ -0,0 +1,64 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "Foo"
},
"components": {
"callbacks": {
"Test": {
"post": {
"operationId": "testCallback",
"description": "Test callback.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "TestTitle",
"type": "object",
"description": "Test description",
"properties": {
"type": {
"type": "string",
"description": "The type of response.",
"enum": [
"TestResponse.Complete"
]
},
"status": {
"type": "string",
"enum": [
"FAILURE",
"SUCCESS"
]
}
},
"required": [
"status"
]
}
}
}
},
"parameters": [
{
"name": "X-Test-Header",
"in": "header",
"required": true,
"example": "1",
"description": "This is a test header parameter",
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Test response."
}
}
}
}
}
}
}

View File

@ -0,0 +1,26 @@
import { CallbackModel } from '../../models/Callback';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
const opts = new RedocNormalizedOptions({});
describe('Models', () => {
describe('CallbackModel', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const spec = require('../fixtures/callback.json');
const parser = new OpenAPIParser(spec, undefined, opts);
test('basic callback details', () => {
const callback = new CallbackModel(
parser,
'Test.Callback',
{ $ref: '#/components/callbacks/Test' },
'',
opts,
);
expect(callback.name).toEqual('Test.Callback');
expect(callback.operations.length).toEqual(0);
expect(callback.expanded).toBeUndefined();
});
});
});

View File

@ -0,0 +1,56 @@
import { action, observable } from 'mobx';
import { OpenAPICallback, Referenced } from '../../types';
import { isOperationName, JsonPointer } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser';
import { OperationModel } from './Operation';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class CallbackModel {
@observable
expanded: boolean;
name: string;
operations: OperationModel[] = [];
constructor(
parser: OpenAPIParser,
name: string,
infoOrRef: Referenced<OpenAPICallback>,
pointer: string,
options: RedocNormalizedOptions,
) {
this.name = name;
const paths = parser.deref<OpenAPICallback>(infoOrRef);
parser.exitRef(infoOrRef);
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];
const operation = new OperationModel(
parser,
{
...operationInfo,
pathName,
pointer: JsonPointer.compile([pointer, name, pathName, operationName]),
httpVerb: operationName,
pathParameters: path.parameters || [],
pathServers: path.servers,
},
undefined,
options,
true,
);
this.operations.push(operation);
}
}
}
@action
toggle() {
this.expanded = !this.expanded;
}
}

View File

@ -4,19 +4,13 @@ import { IMenuItem } from '../MenuStore';
import { GroupModel } from './Group.model';
import { SecurityRequirementModel } from './SecurityRequirement';
import {
OpenAPIExternalDocumentation,
OpenAPIPath,
OpenAPIServer,
OpenAPIXCodeSample,
} from '../../types';
import { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types';
import {
extractExtensions,
getOperationSummary,
getStatusCodeType,
isStatusCode,
JsonPointer,
memoize,
mergeParams,
normalizeServers,
@ -26,12 +20,13 @@ import {
import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { CallbackModel } from './Callback';
import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent';
import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response';
interface XPayloadSample {
export interface XPayloadSample {
lang: 'payload';
label: string;
requestBodyContent: MediaContentModel;
@ -77,23 +72,17 @@ export class OperationModel implements IMenuItem {
servers: OpenAPIServer[];
security: SecurityRequirementModel[];
extensions: Dict<any>;
isCallback: boolean;
constructor(
private parser: OpenAPIParser,
private operationSpec: ExtendedOpenAPIOperation,
parent: GroupModel | undefined,
private options: RedocNormalizedOptions,
isCallback: boolean = false,
) {
this.pointer = JsonPointer.compile(['paths', operationSpec.pathName, operationSpec.httpVerb]);
this.pointer = operationSpec.pointer;
this.id =
operationSpec.operationId !== undefined
? 'operation/' + operationSpec.operationId
: parent !== undefined
? parent.id + this.pointer
: this.pointer;
this.name = getOperationSummary(operationSpec);
this.description = operationSpec.description;
this.parent = parent;
this.externalDocs = operationSpec.externalDocs;
@ -103,20 +92,37 @@ export class OperationModel implements IMenuItem {
this.deprecated = !!operationSpec.deprecated;
this.operationId = operationSpec.operationId;
this.path = operationSpec.pathName;
this.isCallback = isCallback;
const pathInfo = parser.byRef<OpenAPIPath>(
JsonPointer.compile(['paths', operationSpec.pathName]),
this.name = getOperationSummary(operationSpec);
if (this.isCallback) {
// NOTE: Callbacks by default should not inherit the specification's global `security` definition.
// Can be defined individually per-callback in the specification. Defaults to none.
this.security = (operationSpec.security || []).map(
security => new SecurityRequirementModel(security, parser),
);
this.servers = normalizeServers(
parser.specUrl,
operationSpec.servers || (pathInfo && pathInfo.servers) || parser.spec.servers || [],
);
// TODO: update getting pathInfo for overriding servers on path level
this.servers = normalizeServers('', operationSpec.servers || operationSpec.pathServers || []);
} else {
this.id =
operationSpec.operationId !== undefined
? 'operation/' + operationSpec.operationId
: parent !== undefined
? parent.id + this.pointer
: this.pointer;
this.security = (operationSpec.security || parser.spec.security || []).map(
security => new SecurityRequirementModel(security, parser),
);
this.servers = normalizeServers(
parser.specUrl,
operationSpec.servers || operationSpec.pathServers || parser.spec.servers || [],
);
}
if (options.showExtensions) {
this.extensions = extractExtensions(operationSpec, options.showExtensions);
}
@ -138,6 +144,14 @@ export class OperationModel implements IMenuItem {
this.active = false;
}
/**
* Toggle expansion in middle panel (for callbacks, which are operations)
*/
@action
toggle() {
this.expanded = !this.expanded;
}
expand() {
if (this.parent) {
this.parent.expand();
@ -224,4 +238,17 @@ export class OperationModel implements IMenuItem {
);
});
}
@memoize
get callbacks() {
return Object.keys(this.operationSpec.callbacks || []).map(callbackEventName => {
return new CallbackModel(
this.parser,
callbackEventName,
this.operationSpec.callbacks![callbackEventName],
this.pointer,
this.options,
);
});
}
}

View File

@ -10,3 +10,4 @@ export * from './Schema';
export * from './Field';
export * from './ApiInfo';
export * from './SecuritySchemes';
export * from './Callback';

View File

@ -10,9 +10,7 @@ const {
createGlobalStyle,
keyframes,
ThemeProvider,
} = (styledComponents as any) as styledComponents.ThemedStyledComponentsModule<
ResolvedThemeInterface
>;
} = styledComponents as styledComponents.ThemedStyledComponentsModule<ResolvedThemeInterface>;
export const media = {
lessThan(breakpoint, print?: boolean) {

View File

@ -37,6 +37,10 @@ const defaultTheme: ThemeInterface = {
dark: ({ colors }) => darken(colors.tonalOffset, colors.error.main),
contrastText: ({ colors }) => readableColor(colors.error.main),
},
gray: {
50: '#FAFAFA',
100: '#F5F5F5',
},
text: {
primary: '#333333',
secondary: ({ colors }) => lighten(colors.tonalOffset, colors.text.primary),
@ -229,6 +233,10 @@ export interface ResolvedThemeInterface {
success: ColorSetting;
warning: ColorSetting;
error: ColorSetting;
gray: {
50: string;
100: string;
};
border: {
light: string;
dark: string;

View File

@ -196,7 +196,7 @@ export interface OpenAPILink {
export type OpenAPIHeader = Omit<OpenAPIParameter, 'in' | 'name'>;
export interface OpenAPICallback {
$ref?: string;
[name: string]: OpenAPIPath;
}
export interface OpenAPIComponents {

View File

@ -8,9 +8,9 @@ const getInnerHtml = require('./helpers').getInnerHtml;
const URL = 'index.html';
function waitForInit() {
var EC = protractor.ExpectedConditions;
var $apiInfo = $('api-info');
var $errorMessage = $('.redoc-error')
const EC = protractor.ExpectedConditions;
const $apiInfo = $('api-info');
const $errorMessage = $('.redoc-error');
browser.wait(EC.or(EC.visibilityOf($apiInfo), EC.visibilityOf($errorMessage)), 60000);
}
@ -21,7 +21,7 @@ function basicTests(swaggerUrl, title) {
specUrl += `?url=${encodeURIComponent(swaggerUrl)}`;
}
beforeEach((done) => {
beforeEach(done => {
browser.get(specUrl);
waitForInit();
fixFFTest(done);
@ -31,11 +31,11 @@ function basicTests(swaggerUrl, title) {
verifyNoBrowserErrors();
});
it('should init redoc without errors', (done) => {
let $redoc = $('redoc');
it('should init redoc without errors', done => {
const $redoc = $('redoc');
expect($redoc.isPresent()).toBe(true);
setTimeout(() => {
let $operations = $$('operation');
const $operations = $$('operation');
expect($operations.count()).toBeGreaterThan(0);
done();
});
@ -45,11 +45,10 @@ function basicTests(swaggerUrl, title) {
basicTests(null, 'Extended Petstore');
describe('Scroll sync', () => {
let specUrl = URL;
const specUrl = URL;
beforeEach((done) => {
beforeEach(done => {
browser.get(specUrl);
waitForInit();
fixFFTest(done);
@ -57,25 +56,31 @@ describe('Scroll sync', () => {
it('should update active menu entries on page scroll forwards', () => {
scrollToEl('[section="tag/store"]').then(() => {
expect(getInnerHtml('.menu-item.menu-item-depth-1.active > .menu-item-header')).toContain('store');
expect(getInnerHtml('.menu-item.menu-item-depth-1.active > .menu-item-header')).toContain(
'store',
);
expect(getInnerHtml('.selected-tag')).toContain('store');
});
});
it('should update active menu entries on page scroll backwards', () => {
scrollToEl('[operation-id="getPetById"]').then(() => {
expect(getInnerHtml('.menu-item.menu-item-depth-1.active .menu-item-header')).toContain('pet');
expect(getInnerHtml('.menu-item.menu-item-depth-1.active .menu-item-header')).toContain(
'pet',
);
expect(getInnerHtml('.selected-tag')).toContain('pet');
expect(getInnerHtml('.menu-item.menu-item-depth-2.active .menu-item-header')).toContain('Find pet by ID');
expect(getInnerHtml('.menu-item.menu-item-depth-2.active .menu-item-header')).toContain(
'Find pet by ID',
);
expect(getInnerHtml('.selected-endpoint')).toContain('Find pet by ID');
});
});
});
describe('Language tabs sync', () => {
let specUrl = URL;
const specUrl = URL;
beforeEach((done) => {
beforeEach(done => {
browser.get(specUrl);
waitForInit();
fixFFTest(done);
@ -84,10 +89,10 @@ describe('Language tabs sync', () => {
// skip as it fails for no reason on IE on sauce-labs
// TODO: fixme
xit('should sync language tabs', () => {
var $item = $$('[operation-id="addPet"] tabs > ul > li').last();
const $item = $$('[operation-id="addPet"] tabs > ul > li').last();
// check if correct item
expect($item.getText()).toContain('PHP');
var EC = protractor.ExpectedConditions;
const EC = protractor.ExpectedConditions;
browser.wait(EC.elementToBeClickable($item), 5000);
$item.click().then(() => {
expect($('[operation-id="updatePet"] li.active').getText()).toContain('PHP');
@ -97,7 +102,6 @@ describe('Language tabs sync', () => {
if (process.env.JOB === 'e2e-guru') {
describe('APIs.guru specs test', () => {
// global.apisGuruList was loaded in onPrepare method of protractor config
let apisGuruList = global.apisGuruList;
@ -118,11 +122,11 @@ if (process.env.JOB === 'e2e-guru') {
console.log('Running on a short APIs guru list');
apisGuruList = eachNth(apisGuruList, 20);
} else {
console.log('Running on full APIs guru list')
console.log('Running on full APIs guru list');
}
for (let apiName of Object.keys(apisGuruList)) {
let apiInfo = apisGuruList[apiName].versions[apisGuruList[apiName].preferred];
for (const apiName of Object.keys(apisGuruList)) {
const apiInfo = apisGuruList[apiName].versions[apisGuruList[apiName].preferred];
let url = apiInfo.swaggerUrl;
// temporary hack due to this issue: https://github.com/substack/https-browserify/issues/6