diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..fbbb6ff1 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,21 @@ +name: Unit Tests + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: 10.x + - name: yarn install, build, and test + run: | + npm install -g yarn + yarn install + yarn bundle + yarn test diff --git a/.gitignore b/.gitignore index 50c44754..7fa86d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ cli/index.js stats.json /package-lock.json /.idea/ + +# npmrc for local npm +.npmrc diff --git a/CHANGELOG.md b/CHANGELOG.md index a3c424a1..a948efbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +# [2.0.0-rc.16](https://github.com/Redocly/redoc/compare/v2.0.0-rc.15...v2.0.0-rc.16) (2019-09-30) + + +### Bug Fixes + +* fix scrollYOffset when SSR ([d09c1c1](https://github.com/Redocly/redoc/commit/d09c1c1)) + + + +# [2.0.0-rc.15](https://github.com/Redocly/redoc/compare/v2.0.0-rc.14...v2.0.0-rc.15) (2019-09-30) + + +### Bug Fixes + +* auth section appears twice ([5aa7784](https://github.com/Redocly/redoc/commit/5aa7784)), closes [#818](https://github.com/Redocly/redoc/issues/818) +* clicking on group title breaks first tag ([4649683](https://github.com/Redocly/redoc/commit/4649683)), closes [#1034](https://github.com/Redocly/redoc/issues/1034) +* do not crash on empty scopes ([e787d9e](https://github.com/Redocly/redoc/commit/e787d9e)), closes [#1044](https://github.com/Redocly/redoc/issues/1044) +* false-positive recursive detection with allOf at the same level ([faa74d6](https://github.com/Redocly/redoc/commit/faa74d6)) +* fix scrollYOffset when SSR ([21258a5](https://github.com/Redocly/redoc/commit/21258a5)) +* left menu item before group is not highligted ([67e2a8f](https://github.com/Redocly/redoc/commit/67e2a8f)), closes [#1033](https://github.com/Redocly/redoc/issues/1033) +* remove excessive whitespace between md sections on small screens ([e318fb3](https://github.com/Redocly/redoc/commit/e318fb3)), closes [#874](https://github.com/Redocly/redoc/issues/874) +* use url-template dependency ([#1008](https://github.com/Redocly/redoc/issues/1008)) ([32a464a](https://github.com/Redocly/redoc/commit/32a464a)), closes [#1007](https://github.com/Redocly/redoc/issues/1007) + + +### Features + +* **cli:** added support for JSON string value for --options CLI argument ([#1047](https://github.com/Redocly/redoc/issues/1047)) ([2a28130](https://github.com/Redocly/redoc/commit/2a28130)), closes [#797](https://github.com/Redocly/redoc/issues/797) +* **cli:** add `disableGoogleFont` parameter to cli ([#1045](https://github.com/Redocly/redoc/issues/1045)) ([aceb343](https://github.com/Redocly/redoc/commit/aceb343)) +* new option expandDefaultServerVariables ([#1014](https://github.com/Redocly/redoc/issues/1014)) ([0360dce](https://github.com/Redocly/redoc/commit/0360dce)) + + + + # [2.0.0-rc.14](https://github.com/Redocly/redoc/compare/v2.0.0-rc.13...v2.0.0-rc.14) (2019-08-07) diff --git a/README.md b/README.md index a58c19cb..4be4fd16 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ For npm: Install peer dependencies required by ReDoc if you don't have them installed already: - npm i react react-dom mobx@^4.2.0 styled-components + npm i react react-dom mobx@^4.2.0 styled-components core-js Import `RedocStandalone` component from 'redoc' module: @@ -246,6 +246,7 @@ You can use all of the following options with standalone version on tag * `onlyRequiredInSamples` - shows only required fields in request samples. * `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`. * `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false` +* `expandDefaultServerVariables` - enable expanding default server variables, default `false` * `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts) ## Advanced usage of standalone version diff --git a/cli/index.ts b/cli/index.ts index 67f7faad..7bb27bb2 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -25,6 +25,7 @@ interface Options { cdn?: boolean; output?: string; title?: string; + disableGoogleFont?: boolean; port?: number; templateFileName?: string; templateOptions?: any; @@ -68,9 +69,11 @@ YargsParser.command( watch: argv.watch as boolean, templateFileName: argv.template as string, templateOptions: argv.templateOptions || {}, - redocOptions: argv.options || {}, + redocOptions: getObjectOrJSON(argv.options), }; + console.log(config); + try { await serve(argv.port as number, argv.spec as string, config); } catch (e) { @@ -99,6 +102,12 @@ YargsParser.command( default: 'ReDoc documentation', }); + yargs.options('disableGoogleFont', { + describe: 'Disable Google Font', + type: 'boolean', + default: false, + }); + yargs.option('cdn', { describe: 'Do not include ReDoc source code into html page, use link to CDN instead', type: 'boolean', @@ -108,15 +117,16 @@ YargsParser.command( yargs.demandOption('spec'); return yargs; }, - async argv => { - const config: Options = { + async (argv: any) => { + const config = { ssr: true, output: argv.o as string, cdn: argv.cdn as boolean, title: argv.title as string, + disableGoogleFont: argv.disableGoogleFont as boolean, templateFileName: argv.template as string, templateOptions: argv.templateOptions || {}, - redocOptions: argv.options || {}, + redocOptions: getObjectOrJSON(argv.options), }; try { @@ -180,21 +190,34 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) { if (options.watch && existsSync(pathToSpec)) { const pathToSpecDirectory = resolve(dirname(pathToSpec)); const watchOptions = { - ignored: /(^|[\/\\])\../, + ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/], + ignoreInitial: true, }; const watcher = watch(pathToSpecDirectory, watchOptions); const log = console.log.bind(console); + + const handlePath = async path => { + try { + spec = await loadAndBundleSpec(pathToSpec); + pageHTML = await getPageHTML(spec, pathToSpec, options); + log('Updated successfully'); + } catch (e) { + console.error('Error while updating: ', e.message); + } + }; + watcher .on('change', async path => { log(`${path} changed, updating docs`); - try { - spec = await loadAndBundleSpec(pathToSpec); - pageHTML = await getPageHTML(spec, pathToSpec, options); - log('Updated successfully'); - } catch (e) { - console.error('Error while updating: ', e.message); - } + handlePath(path); + }) + .on('add', async path => { + log(`File ${path} added, updating docs`); + handlePath(path); + }) + .on('addDir', path => { + log(`↗ Directory ${path} added. Files in here will trigger reload.`); }) .on('error', error => console.error(`Watcher error: ${error}`)) .on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`)); @@ -218,7 +241,15 @@ async function bundle(pathToSpec, options: Options = {}) { async function getPageHTML( spec: any, pathToSpec: string, - { ssr, cdn, title, templateFileName, templateOptions, redocOptions = {} }: Options, + { + ssr, + cdn, + title, + disableGoogleFont, + templateFileName, + templateOptions, + redocOptions = {}, + }: Options, ) { let html; let css; @@ -261,6 +292,7 @@ async function getPageHTML( : ``) + css : '', title, + disableGoogleFont, templateOptions, }); } @@ -323,3 +355,15 @@ function handleError(error: Error) { console.error(error.stack); process.exit(1); } + +function getObjectOrJSON(options) { + try { + return options && typeof options === 'string' + ? JSON.parse(options) : options + ? options + : {}; + } catch (e) { + console.log(`Encountered error:\n${options}\nis not a valid JSON.`); + handleError(e); + } +} diff --git a/cli/package.json b/cli/package.json index 6823f4b8..0291c544 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "redoc-cli", - "version": "0.8.6", + "version": "0.9.2", "description": "ReDoc's Command Line Interface", "main": "index.js", "bin": "index.js", @@ -19,7 +19,7 @@ "node-libs-browser": "^2.2.1", "react": "^16.8.6", "react-dom": "^16.8.6", - "redoc": "2.0.0-rc.13", + "redoc": "2.0.0-rc.16", "styled-components": "^4.3.2", "tslib": "^1.10.0", "yargs": "^13.3.0" diff --git a/cli/template.hbs b/cli/template.hbs index fff76799..f0a44029 100644 --- a/cli/template.hbs +++ b/cli/template.hbs @@ -13,7 +13,7 @@ } {{{redocHead}}} - + {{#unless disableGoogleFont}}{{/unless}} diff --git a/cli/yarn.lock b/cli/yarn.lock index fe2f4435..7e1362f3 100644 --- a/cli/yarn.lock +++ b/cli/yarn.lock @@ -629,10 +629,10 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -dompurify@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-1.0.11.tgz#fe0f4a40d147f7cebbe31a50a1357539cfc1eb4d" - integrity sha512-XywCTXZtc/qCX3iprD1pIklRVk/uhl8BKpkTxr+ZyMVUzSUg7wkQXRBp/euJ5J5moa1QvfpvaPQVP71z1O59dQ== +dompurify@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.3.tgz#5cc4965a487d54aedba6ba9634b137cfbd7eb50d" + integrity sha512-q006uOkD2JGSJgF0qBt7rVhUvUPBWCxpGayALmHvXx2iNlMfNVz7PDGeXEUjNGgIDjADz59VZCv6UE3U8XRWVw== elliptic@^6.0.0: version "6.5.0" @@ -1092,7 +1092,7 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memoize-one@^5.0.0, memoize-one@^5.0.5: +memoize-one@^5.0.0, memoize-one@~5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.5.tgz#8cd3809555723a07684afafcd6f756072ac75d7e" integrity sha512-ey6EpYv0tEaIbM/nTDOpHciXUvd+ackQrJgEzBwemhZZIWZjcyodqEcrmqDy2BKRTM3a65kKBV4WtLXJDt26SQ== @@ -1161,10 +1161,10 @@ mobx-react-lite@1.4.0: resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-1.4.0.tgz#193beb5fdddf17ae61542f65ff951d84db402351" integrity sha512-5xCuus+QITQpzKOjAOIQ/YxNhOl/En+PlNJF+5QU4Qxn9gnNMJBbweAdEW3HnuVQbfqDYEUnkGs5hmkIIStehg== -mobx-react@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.1.1.tgz#24a2c8a3393890fa732b4efd34cc6dcccf6e0e7a" - integrity sha512-hjACWCTpxZf9Sv1YgWF/r6HS6Nsly1SYF22qBJeUE3j+FMfoptgjf8Zmcx2d6uzA07Cezwap5Cobq9QYa0MKUw== +mobx-react@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.1.3.tgz#ad07880ea60cdcdb2a7e2a0d54e01379710cf00a" + integrity sha512-eT/jO9dYIoB1AlZwI2VC3iX0gPOeOIqZsiwg7tDJV1B7Z69h+TZZL3dgOE0UeS2zoHhGeKbP+K+OLeLMnnkGnA== dependencies: mobx-react-lite "1.4.0" @@ -1540,10 +1540,10 @@ react-dropdown@^1.6.4: dependencies: classnames "^2.2.3" -react-hot-loader@^4.12.10: - version "4.12.10" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.10.tgz#b3457c0f733423c4827c6d2672e50c9f8bedaf6b" - integrity sha512-dX+ZUigxQijWLsKPnxc0khuCt2sYiZ1W59LgSBMOLeGSG3+HkknrTlnJu6BCNdhYxbEQkGvBsr7zXlNWYUIhAQ== +react-hot-loader@^4.12.14: + version "4.12.14" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.14.tgz#81ca06ffda0b90aad15d6069339f73ed6428340a" + integrity sha512-ecxH4eBvEaJ9onT8vkEmK1FAAJUh1PqzGqds9S3k+GeihSp7nKAp4fOxytO+Ghr491LiBD38jaKyDXYnnpI9pQ== dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" @@ -1602,35 +1602,35 @@ readdirp@^3.1.1: dependencies: picomatch "^2.0.4" -redoc@2.0.0-rc.13: - version "2.0.0-rc.13" - resolved "https://registry.yarnpkg.com/redoc/-/redoc-2.0.0-rc.13.tgz#243e4d003ca9bd45006c215d8856a3b1229ca8bb" - integrity sha512-t0vlss1TIUknYXTI9RIZ1nRMyIW/pjo4KMMDFOMdRq5/8jopkNyf37q25BwBuAJfDxQV+tIUoy6o+rAAffeDkQ== +redoc@2.0.0-rc.16: + version "2.0.0-rc.16" + resolved "https://registry.yarnpkg.com/redoc/-/redoc-2.0.0-rc.16.tgz#01d5dafba6ae266a5934dc9904b87bc8a175b222" + integrity sha512-5YWk7NBebYZ8xMbKXA1sD++QsSh7NbnB2sStJRKLeP/rU4oX586SIqHXl+MW1OhIZW44mYFMHpYzxpZKCllk9w== dependencies: classnames "^2.2.6" decko "^1.2.0" - dompurify "^1.0.11" + dompurify "^2.0.3" eventemitter3 "^4.0.0" json-pointer "^0.6.0" json-schema-ref-parser "^6.1.0" lunr "2.3.6" mark.js "^8.11.1" marked "^0.7.0" - memoize-one "^5.0.5" - mobx-react "^6.1.1" + memoize-one "~5.0.5" + mobx-react "^6.1.3" openapi-sampler "1.0.0-beta.15" perfect-scrollbar "^1.4.0" polished "^3.4.1" prismjs "^1.17.1" prop-types "^15.7.2" react-dropdown "^1.6.4" - react-hot-loader "^4.12.10" + react-hot-loader "^4.12.14" react-tabs "^3.0.0" - slugify "^1.3.4" + slugify "^1.3.5" stickyfill "^1.1.1" swagger2openapi "^5.3.1" tslib "^1.10.0" - uri-template-lite "^19.4.0" + url-template "^2.0.8" reftools@^1.0.8: version "1.0.8" @@ -1782,10 +1782,10 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= -slugify@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.4.tgz#78d2792d7222b55cd9fc81fa018df99af779efeb" - integrity sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw== +slugify@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.5.tgz#90210678818b6d533cb060083aed0e8238133508" + integrity sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA== source-map@^0.5.0: version "0.5.7" @@ -2002,10 +2002,10 @@ uglify-js@^3.1.4: commander "~2.20.0" source-map "~0.6.1" -uri-template-lite@^19.4.0: - version "19.4.0" - resolved "https://registry.yarnpkg.com/uri-template-lite/-/uri-template-lite-19.4.0.tgz#cbc2c072cf4931428a2f9d3aea36b8254a33cce5" - integrity sha512-VY8dgwyMwnCztkzhq0cA/YhNmO+YZqow//5FdmgE2fZU/JPi+U0rPL7MRDi0F+Ch4vJ7nYidWzeWAeY7uywe9g== +url-template@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" + integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= url@^0.11.0: version "0.11.0" diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..3e44267f --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1 @@ +intent.json \ No newline at end of file diff --git a/demo/openapi.yaml b/demo/openapi.yaml index b7c1833a..574fb379 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -1,8 +1,8 @@ openapi: 3.0.0 servers: - - url: //petstore.swagger.io/v2 + - url: https://petstore.swagger.io/v2 description: Default server - - url: //petstore.swagger.io/sandbox + - url: https://petstore.swagger.io/sandbox description: Sandbox server info: description: | @@ -38,8 +38,6 @@ info: OAuth2 - an open protocol to allow secure authorization in a simple and standard method from web, mobile and desktop applications. - - version: 1.0.0 title: Swagger Petstore termsOfService: 'http://swagger.io/terms/' @@ -103,7 +101,7 @@ paths: '405': description: Invalid input security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' x-code-samples: @@ -151,7 +149,7 @@ paths: '405': description: Validation exception security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' x-code-samples: @@ -219,7 +217,7 @@ paths: '405': description: Invalid input security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' requestBody: @@ -258,7 +256,7 @@ paths: '400': description: Invalid pet value security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' '/pet/{petId}/uploadImage': @@ -284,7 +282,7 @@ paths: schema: $ref: '#/components/schemas/ApiResponse' security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' requestBody: @@ -334,7 +332,7 @@ paths: '400': description: Invalid status value security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' /pet/findByTags: @@ -374,7 +372,7 @@ paths: '400': description: Invalid tag value security: - - petstore_auth: + - authorization: - 'write:pets' - 'read:pets' /store/inventory: @@ -925,7 +923,7 @@ components: description: List of user object required: true securitySchemes: - petstore_auth: + authorization: description: | Get access to data while protecting your account credentials. OAuth2 is also a safer and more secure way to give you access. diff --git a/demo/petstore.json b/demo/petstore.json new file mode 100644 index 00000000..77a25e0d --- /dev/null +++ b/demo/petstore.json @@ -0,0 +1,993 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [{ + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + }], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + }], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + }, + "collectionFormat": "multi" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [{ + "api_key": [ + + ] + }] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json" + ], + "parameters": [ + + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [{ + "api_key": [ + + ] + }] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10.0, + "minimum": 1.0, + "format": "int64" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1.0, + "format": "int64" + }], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + }], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + }], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + }], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + }], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } +} diff --git a/demo/playground/hmr-playground.tsx b/demo/playground/hmr-playground.tsx index adca6279..4f98b0c4 100644 --- a/demo/playground/hmr-playground.tsx +++ b/demo/playground/hmr-playground.tsx @@ -25,7 +25,8 @@ const specUrl = (userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml'); let store; -const options: RedocRawOptions = { nativeScrollbars: false }; +const headers = {}; +const options: RedocRawOptions = { nativeScrollbars: false, enableConsole: true, providedByName: 'Intent ApiDocs by Nutanix', providedByUri: 'http://www.nutanix.com', additionalHeaders: headers }; async function init() { const spec = await loadAndBundleSpec(specUrl); diff --git a/demo/ssr/index.ts b/demo/ssr/index.ts index baac5331..142a58ef 100644 --- a/demo/ssr/index.ts +++ b/demo/ssr/index.ts @@ -1,10 +1,10 @@ -import { renderToString } from 'react-dom/server'; -import * as React from 'react'; -import { ServerStyleSheet } from 'styled-components'; -// @ts-ignore -import { Redoc, createStore } from '../../'; import { readFileSync } from 'fs'; import { resolve } from 'path'; +import * as React from 'react'; +import { renderToString } from 'react-dom/server'; +import { ServerStyleSheet } from 'styled-components'; + +import { createStore, Redoc } from '../../'; const yaml = require('yaml-js'); const http = require('http'); @@ -19,7 +19,7 @@ const server = http.createServer(async (request, response) => { fs.createReadStream('bundles/redoc.standalone.js', 'utf8').pipe(response); } else if (request.url === '/') { const spec = yaml.load(readFileSync(resolve(__dirname, '../openapi.yaml'))); - let store = await createStore(spec, 'path/to/spec.yaml'); + const store = await createStore(spec, 'path/to/spec.yaml'); const sheet = new ServerStyleSheet(); diff --git a/demo/webpack.config.ts b/demo/webpack.config.ts index d48443ae..0b36e52a 100644 --- a/demo/webpack.config.ts +++ b/demo/webpack.config.ts @@ -56,6 +56,26 @@ const babelHotLoader = { }, }; +let proxy = {}; +let https = false; +//we are using our own proxy here +if (process.env.PROXY && process.env.USERNAME && process.env.PASSWORD) { + proxy = { + "/api": { + auth: `${process.env.USERNAME}:${process.env.PASSWORD}`, + target: process.env.PROXY, + "secure": false, + changeOrigin: true, + ws: true, + xfwd: true + } + } + https = true; + console.log('Using proxy configuration provided with command line, https in use.\n'); +} else { + console.log('Using proxy from PC/PE local server\n'); +} + export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({ entry: [ root('../src/polyfills.ts'), @@ -79,8 +99,12 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) = port: 9090, disableHostCheck: true, stats: 'minimal', + https, + proxy, }, + devtool: 'source-map', + resolve: { extensions: ['.ts', '.tsx', '.js', '.json'], alias: diff --git a/package.json b/package.json index 5b199dcf..888ec446 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redoc", - "version": "2.0.0-rc.14", + "version": "2.0.0-rc.16", "description": "ReDoc", "repository": { "type": "git", @@ -70,7 +70,9 @@ "@types/mark.js": "^8.11.4", "@types/marked": "^0.6.5", "@types/prismjs": "^1.16.0", + "@types/promise": "^7.1.30", "@types/prop-types": "^15.7.1", + "@types/qs": "^6.5.1", "@types/react": "^16.8.23", "@types/react-dom": "^16.8.5", "@types/react-hot-loader": "^4.1.0", @@ -133,6 +135,9 @@ "styled-components": "^4.1.1" }, "dependencies": { + "ajv": "^6.4.0", + "ajv-errors": "^1.0.0", + "brace": "^0.11.1", "classnames": "^2.2.6", "decko": "^1.2.0", "dompurify": "^1.0.11", @@ -149,14 +154,18 @@ "polished": "^3.4.1", "prismjs": "^1.17.1", "prop-types": "^15.7.2", + "qs": "^6.5.2", + "react-ace": "^6.0.0", "react-dropdown": "^1.6.4", "react-hot-loader": "^4.12.10", + "react-switch": "^5.0.1", "react-tabs": "^3.0.0", "slugify": "^1.3.4", "stickyfill": "^1.1.1", "swagger2openapi": "^5.3.1", "tslib": "^1.10.0", - "uri-template-lite": "^19.4.0" + "uri-template-lite": "^19.4.0", + "url-template": "^2.0.8" }, "bundlesize": [ { diff --git a/src/common-elements/SwitchBox.tsx b/src/common-elements/SwitchBox.tsx new file mode 100644 index 00000000..dc026c04 --- /dev/null +++ b/src/common-elements/SwitchBox.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import Switch from 'react-switch'; +import {FlexLayout} from './index'; + +import styled from '../styled-components'; + +const CustomFlexLayout = styled(FlexLayout)` + align-items: center; +`; + +interface LabelProps { + active: boolean; +} + +const Label = styled.label` + color: ${props => props.active ? props.theme.colors.success.main : props.theme.colors.text.secondary} + margin-left: 10px; + font-size: 120%; +`; + +interface TryItOutProps { + label: string; + checked: boolean; + onClick: () => void; +} + +export class SwitchBox extends React.PureComponent { + id = 'toggle-id-' + Date.now(); + render() { + const { label, checked, onClick } = this.props; + return ( + + + + + ); + } +} diff --git a/src/common-elements/buttons.ts b/src/common-elements/buttons.ts new file mode 100644 index 00000000..bbdc6524 --- /dev/null +++ b/src/common-elements/buttons.ts @@ -0,0 +1,23 @@ +import styled from '../styled-components'; + +export const Button = styled.button` + background: #248fb2; + border-radius: 0px; + border: none; + color: white; + font-size: 0.929em; + padding: 5px; +`; + +export const SubmitButton = styled(Button)` + background: ${props => props.theme.colors.primary.main} + padding: 10px 30px; + border-radius: 4px; + cursor: pointer; + text-align: center; + outline: none; + margin: 1em 0; + min-width: 60px; + font-weight: bold; + order: 1; +`; diff --git a/src/common-elements/index.ts b/src/common-elements/index.ts index 5c9ec542..3ff7e1cc 100644 --- a/src/common-elements/index.ts +++ b/src/common-elements/index.ts @@ -9,3 +9,6 @@ export * from './mixins'; export * from './tabs'; export * from './samples'; export * from './perfect-scrollbar'; +export * from './toggle'; +export * from './input'; +export * from './buttons'; diff --git a/src/common-elements/input.tsx b/src/common-elements/input.tsx new file mode 100644 index 00000000..a6cd9bf5 --- /dev/null +++ b/src/common-elements/input.tsx @@ -0,0 +1,8 @@ +import styled from '../styled-components'; + +export const TextField = styled.input` + padding: 0.5em; + margin: 0.5em; + border: 1px solid rgba(38,50,56,0.5); + border-radius: 3px; +`; diff --git a/src/common-elements/panels.ts b/src/common-elements/panels.ts index f41845fc..f566ff35 100644 --- a/src/common-elements/panels.ts +++ b/src/common-elements/panels.ts @@ -1,16 +1,18 @@ import { SECTION_ATTR } from '../services/MenuStore'; import styled, { media } from '../styled-components'; -export const MiddlePanel = styled.div` +export const MiddlePanel = styled.div<{ compact?: boolean }>` width: calc(100% - ${props => props.theme.rightPanel.width}); padding: 0 ${props => props.theme.spacing.sectionHorizontal}px; direction: ${props => props.theme.typography.direction || 'ltr'}; text-align: ${props => (props.theme.typography.direction === 'rtl') ? 'right' : 'inherit'}; - ${media.lessThan('medium', true)` + ${({ compact, theme }) => + media.lessThan('medium', true)` width: 100%; - padding: ${props => - `${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`}; + padding: ${`${compact ? 0 : theme.spacing.sectionVertical}px ${ + theme.spacing.sectionHorizontal + }px`}; `}; `; @@ -73,3 +75,13 @@ export const Row = styled.div` flex-direction: column; `}; `; + +export const FlexLayout = styled.div` + align-items: flex-end; + display: flex; + width: 100%; +`; + +export const FlexLayoutReverse = styled(FlexLayout)` + flex-direction: row-reverse; +`; diff --git a/src/common-elements/toggle.tsx b/src/common-elements/toggle.tsx new file mode 100644 index 00000000..8b782008 --- /dev/null +++ b/src/common-elements/toggle.tsx @@ -0,0 +1,10 @@ +import styled from '../styled-components'; + +export const Toggle = styled.input` + padding: 0.5em; + margin: 0.5em; + color: palevioletred; + background: papayawhip; + border: none; + border-radius: 3px; +`; diff --git a/src/components/Console/ConsoleEditor.tsx b/src/components/Console/ConsoleEditor.tsx new file mode 100644 index 00000000..9e9091b7 --- /dev/null +++ b/src/components/Console/ConsoleEditor.tsx @@ -0,0 +1,78 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; + +import AceEditor from 'react-ace'; + +import 'brace/mode/curly'; +import 'brace/mode/json'; +import 'brace/theme/github'; +import 'brace/theme/monokai'; + +import {MediaTypeModel} from '../../services/models'; +import {ConsoleEditorWrapper} from './ConsoleEditorWrapper'; + +export interface ConsoleEditorProps { + mediaTypes: MediaTypeModel[]; +} + +@observer +export class ConsoleEditor extends React.Component { + + editor: any; + + render() { + const { mediaTypes } = this.props; + + if (!mediaTypes.length) { + return null; + } + let sample = {}; + for (const mediaType of mediaTypes) { + if (mediaType.name.indexOf('json') > -1) { + if (mediaType.examples) { + const example = getDefaultOrFirst(mediaType.examples); + sample = example && example.value; + } + break; + } + } + + return ( + + (this.editor = ace)} + /> + + ); + } + +} + +function getDefaultOrFirst(object) { + if (typeof object === 'object') { + if (typeof object.default === 'object') { + return object.default; + } else { + return object[Object.keys(object)[0]]; + } + } else { + return false; + } +} diff --git a/src/components/Console/ConsoleEditorWrapper.ts b/src/components/Console/ConsoleEditorWrapper.ts new file mode 100644 index 00000000..314c98e4 --- /dev/null +++ b/src/components/Console/ConsoleEditorWrapper.ts @@ -0,0 +1,77 @@ +import {lighten} from 'polished'; +import styled from '../../styled-components'; + +export const ConsoleEditorWrapper = styled.div` + font-family: ${props => props.theme.typography.code.fontFamily}; + font-size: ${props => props.theme.typography.code.fontSize} !important; + direction: ltr; + white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')}; + contain: content; + overflow-x: auto; + background: #11171a !important; + padding: 5px 0; + + & .ace_editor { + background: #11171a !important; + width: 100% !important; + } + & .ace_editor .ace_marker-layer .ace_selection, & .ace_editor .ace_marker-layer .ace_selected-word { + background: ${lighten(0.05, '#11171a')} !important; + border-color: ${lighten(0.05, '#11171a')} !important; + } + & .ace_editor .ace_marker-layer .ace_active-line { + background: rgba(0, 0, 0, 0.2); + } + & .ace_editor .ace_line, & .ace_editor .ace_cursor { + color: #aaa; + } + & .ace_editor .ace_marker-layer .ace_bracket { + border: none !important; + } + & .ace_editor .ace_line .ace_fold { + background: none !important; + color: #aaa; + border: none; + } + & .ace_editor .ace_line .ace_fold:hover { + background: none !important; + } + & .ace_editor .ace_string { + color: #71e4ff; + } + & .ace_editor .ace_variable { + color: #a0fbaa; + } + & .ace_editor .ace_indent-guide { + background: none; + color: rgba(255, 255, 255, 0.3) + } + & .ace_editor .ace_indent-guide::after { + content: "|"; + } + & .ace_editor .ace_gutter { + background: ${lighten(0.01, '#11171a')} !important; + color: #fff !important; + } + & .ace_editor .ace_gutter .ace_fold-widget { + background-image: none; + } + & .ace_editor .ace_gutter .ace_fold-widget.ace_open::after { + content: "-"; + } + & .ace_editor .ace_gutter .ace_fold-widget.ace_closed::after { + content: "+"; + } + & .ace_editor .ace_gutter .ace_gutter-active-line { + background: rgba(0, 0, 0, 0.2) !important; + } + & .ace_editor .ace_gutter .ace_gutter-cell.ace_error { + background: none !important; + } + & .ace_editor .ace_gutter .ace_gutter-cell.ace_error::before { + position: absolute; + color: red; + content: "X"; + left: 0.5em; + } +`; diff --git a/src/components/Console/ConsoleViewer.tsx b/src/components/Console/ConsoleViewer.tsx new file mode 100644 index 00000000..f5ec4019 --- /dev/null +++ b/src/components/Console/ConsoleViewer.tsx @@ -0,0 +1,172 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { SecuritySchemeModel } from '../../../typings/services/models'; +import { SubmitButton } from '../../common-elements/buttons'; +import { FlexLayoutReverse } from '../../common-elements/panels'; +import { FieldModel, OperationModel, SecuritySchemesModel } from '../../services/models'; +import { ConsoleResponse } from '../ConsoleResponse/Response'; +import { ConsoleEditor } from './ConsoleEditor'; + +const qs = require('qs'); + +export interface ConsoleViewerProps { + operation: OperationModel; + additionalHeaders?: object; + queryParamPrefix?: string; + queryParamSuffix?: string; + securitySchemes: SecuritySchemesModel; + urlIndex: number; +} + +export interface ConsoleViewerState { + result: any; +} + +export interface Schema { + _$ref?: any; +} + +@observer +export class ConsoleViewer extends React.Component { + operation: OperationModel; + additionalHeaders: object; + visited = new Set(); + private consoleEditor: any; + + constructor(props) { + super(props); + this.state = { + result: null, + }; + } + onClickSend = async () => { + const ace = this.consoleEditor && this.consoleEditor.editor; + const { operation, securitySchemes: {schemes}, additionalHeaders = {}, urlIndex = 0 } = this.props; + + let value = ace && ace.editor.getValue(); + + const content = operation.requestBody && operation.requestBody.content; + const mediaType = content && content.mediaTypes[content.activeMimeIdx]; + const endpoint = { + method: operation.httpVerb, + path: operation.servers[urlIndex].url + operation.path, + }; + if (value) { + value = JSON.parse(value); + } + const contentType = mediaType && mediaType.name || 'application/json'; + const contentTypeHeader = { 'Content-Type': contentType }; + + const schemeMapper: Map = new Map(); + schemes.forEach(scheme => { + schemeMapper.set(scheme.id, scheme); + }); + + const securityHeaders: Dict = {}; + + operation.security.forEach(({schemes: [{ id }]}) => { + if (schemeMapper.has(id)) { + // this part of code needs a ts-ignore because typescript couldn't detect that schemeMapper.get(id) - + // has been checked to avoid token of undefined. + // @ts-ignore + securityHeaders[id] = schemeMapper.get(id).token; + } + }); + const headers = { ...additionalHeaders, ...contentTypeHeader, ...securityHeaders }; + let result; + try { + result = await this.invoke(endpoint, value, headers); + this.setState({ + result, + }); + } catch (error) { + this.setState({ + result: error, + }); + } + }; + + /* + * If we have a url like foo/bar/{uuid} uuid will be replaced with what user has typed in. + */ + addParamsToUrl(url: string, params: FieldModel[]) { + const queryParamPrefix = '{'; + const queryParamSuffix = '}'; + + for (const fieldModel of params) { + if (url.indexOf(`${queryParamPrefix}${fieldModel.name}${queryParamSuffix}`) > -1 && fieldModel.$value.length > 0) { + url = url.replace(`${queryParamPrefix}${fieldModel.name}${queryParamSuffix}`, fieldModel.$value); + } + } + + if (url.split(queryParamPrefix).length > 1) { + throw Error(`** we have missing query params ** ${url}`); + } + + return url; + + } + + async invoke(endpoint, body, headers = {}) { + try { + const { operation } = this.props; + let url = this.addParamsToUrl(endpoint.path, operation.parameters || []); + if (endpoint.method.toLocaleLowerCase() === 'get') { + url = url + '?' + qs.stringify(body || ''); + } + const myHeaders = new Headers(); + for (const [key, value] of Object.entries(headers)) { + myHeaders.append(key, `${value}`); + } + + const request = new Request(url, { + method: endpoint.method, + redirect: 'manual', + headers: myHeaders, + body: (body) ? JSON.stringify(body) : undefined, + }); + + const response = await fetch(request); + const content = await response.json(); + const { ok, status, statusText, redirected } = response; + return { + content, + ok, + status, + statusText, + redirected, + headers: response.headers, + url: response.url, + }; + + } catch (error) { + console.error(error); + } + + } + + render() { + const { operation } = this.props; + const requestBodyContent = operation.requestBody && operation.requestBody.content && operation.requestBody.content; + const hasBodySample = requestBodyContent && requestBodyContent.hasSample; + const mediaTypes = (requestBodyContent && requestBodyContent.mediaTypes) ? requestBodyContent.mediaTypes : []; + const { result } = this.state; + return ( +
+

Request

+ {hasBodySample && ( + (this.consoleEditor = editor)} + /> + )} + + Send Request + + {result && + + } +
+ ); + } +} diff --git a/src/components/Console/index.ts b/src/components/Console/index.ts new file mode 100644 index 00000000..129986fe --- /dev/null +++ b/src/components/Console/index.ts @@ -0,0 +1,2 @@ +export * from './ConsoleEditor'; +export * from './ConsoleViewer'; \ No newline at end of file diff --git a/src/components/ConsoleResponse/Response.tsx b/src/components/ConsoleResponse/Response.tsx new file mode 100644 index 00000000..34726703 --- /dev/null +++ b/src/components/ConsoleResponse/Response.tsx @@ -0,0 +1,119 @@ +import * as React from 'react'; +import { SourceCodeWithCopy } from '..'; +import { RightPanelHeader } from '../../common-elements'; +import styled from '../../styled-components'; + +import { JsonViewer } from '../JsonViewer/JsonViewer'; + +interface ConsoleResponseProps { + response: any; +} + +interface ConsoleResponseState { + collapse: boolean; +} + +export class ConsoleResponse extends React.PureComponent { + + constructor(props) { + super(props); + this.state = { collapse: false}; + } + + changeCollapse = () => { + this.setState({collapse: !this.state.collapse}); + }; + + render() { + const { response: { headers, type, status, statusText, content } } = this.props; + const collapse = this.state.collapse; + return( + <> + status: + {status} {statusText} + Response Payload + + + + Response Headers + + + + + { + collapse && + + + show undocumented response headers + + } + + + ); + } +} + +const HeaderWrapper = styled.div` + color: white; + background-color: ${props => props.theme.codeSample.backgroundColor}; + padding: 10px 0 18px; + margin: 10px 0; + height: 100%; + div div div { + display: none !important; + } + div pre span:first-child { + display: none !important; + } + div pre span:last-child { + display: none !important; + } + div pre { + height: 100%; + overflow: hidden; + } + div { + height: 100%; + } +`; + +const SourceCodeWrapper = styled.div` + &.collapse-false { + height: 89px; + } + &.collapse-true { + height: auto; + } +`; + +const JsonWrapper = styled.div` + color: white; + background-color: ${props => props.theme.codeSample.backgroundColor}; + padding: 10px; + margin: 10px 0; +`; + +const StatusWrapper = styled.div` + &.status-success { + color: #00ff1c; + } + &.status-redirect { + color: ${props => props.theme.colors.responses.redirect.color}; + } + &.status-info { + color: ${props => props.theme.colors.responses.info.color}; + } + &.status-error { + color: ${props => props.theme.colors.responses.error.color}; + } + color: white; + background-color: ${props => props.theme.codeSample.backgroundColor}; + padding: 10px; + margin: 10px 0; +`; + +const ShowMore = styled.div` + text-align: center; + u { + cursor: pointer; + } +`; diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 3b638896..5120d4c8 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -1,35 +1,40 @@ import { observer } from 'mobx-react'; import * as React from 'react'; +import { AppStore } from '../../services'; 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'; +import { GroupModel } from '../../services/models'; import { Operation } from '../Operation/Operation'; -@observer -export class ContentItems extends React.Component<{ +export interface ContentItemsProps { items: ContentItemModel[]; -}> { + store: AppStore; +} + +@observer +export class ContentItems extends React.Component { render() { - const items = this.props.items; + const { items, store } = this.props; if (items.length === 0) { return null; } - return items.map(item => ); + return items.map(item => ); } } export interface ContentItemProps { item: ContentItemModel; + store: AppStore; } @observer export class ContentItem extends React.Component { render() { - const item = this.props.item; + const { item, store } = this.props; let content; const { type } = item; switch (type) { @@ -41,7 +46,7 @@ export class ContentItem extends React.Component { content = ; break; case 'operation': - content = ; + content = ; break; default: content = ; @@ -54,13 +59,13 @@ export class ContentItem extends React.Component { {content} )} - {item.items && } + {item.items && } ); } } -const middlePanelWrap = component => {component}; +const middlePanelWrap = component => {component}; @observer export class SectionItem extends React.Component { @@ -71,7 +76,7 @@ export class SectionItem extends React.Component { return ( <> - +
{name} @@ -90,12 +95,3 @@ export class SectionItem extends React.Component { ); } } - -@observer -export class OperationItem extends React.Component<{ - item: OperationModel; -}> { - render() { - return ; - } -} diff --git a/src/components/Endpoint/Endpoint.tsx b/src/components/Endpoint/Endpoint.tsx index 21bffa52..6b251507 100644 --- a/src/components/Endpoint/Endpoint.tsx +++ b/src/components/Endpoint/Endpoint.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { ShelfIcon } from '../../common-elements'; -import { OperationModel } from '../../services'; +import { ClipboardService, OperationModel } from '../../services'; import { Markdown } from '../Markdown/Markdown'; import { OptionsContext } from '../OptionsProvider'; import { SelectOnClick } from '../SelectOnClick/SelectOnClick'; -import { getBasePath } from '../../utils'; +import { expandDefaultServerVariables, getBasePath } from '../../utils'; import { EndpointInfo, HttpVerb, @@ -21,10 +21,13 @@ export interface EndpointProps { hideHostname?: boolean; inverted?: boolean; + handleUrl: any; } export interface EndpointState { expanded: boolean; + selectedItem: number; + tooltipShown: boolean; } export class Endpoint extends React.Component { @@ -32,6 +35,8 @@ export class Endpoint extends React.Component { super(props); this.state = { expanded: false, + selectedItem: 0, + tooltipShown: false, }; } @@ -48,6 +53,9 @@ export class Endpoint extends React.Component { {options => ( +
+ Copied +
{operation.httpVerb}{' '} {operation.path} @@ -60,16 +68,19 @@ export class Endpoint extends React.Component { /> - {operation.servers.map(server => { + {operation.servers.map((server, index) => { + const normalizedUrl = options.expandDefaultServerVariables + ? expandDefaultServerVariables(server.url, server.variables) + : server.url; return ( - - - + + + {hideHostname || options.hideHostname - ? getBasePath(server.url) - : server.url} + ? getBasePath(normalizedUrl) + : normalizedUrl} {operation.path} @@ -83,4 +94,19 @@ export class Endpoint extends React.Component {
); } + + handleUrl(url: number) { + this.props.handleUrl(url); + this.setState({ + selectedItem: url, + expanded: false, + tooltipShown: true, + }); + ClipboardService.copyCustom(this.props.operation.servers[url].url + this.props.operation.path); + setTimeout(() => { + this.setState({ + tooltipShown: false, + }); + }, 1000); + } } diff --git a/src/components/Endpoint/styled.elements.ts b/src/components/Endpoint/styled.elements.ts index 853f38b1..9913d842 100644 --- a/src/components/Endpoint/styled.elements.ts +++ b/src/components/Endpoint/styled.elements.ts @@ -4,6 +4,26 @@ export const OperationEndpointWrap = styled.div` cursor: pointer; position: relative; margin-bottom: 5px; + .showToolTip { + visibility: initial; + background-color: white; + color: black; + padding: 3px; + position: initial; + width: 53px; + text-align: center; + margin-bottom: 10px; + border-radius: 4px; + }; + .hideToolTip { + visibility:hidden; + padding: 3px; + position: initial; + width: 53px; + text-align: center; + margin-bottom: 10px; + border-radius: 4px; + } `; export const ServerRelativeURL = styled.span` @@ -66,16 +86,28 @@ export const ServersOverlay = styled.div<{ expanded: boolean }>` export const ServerItem = styled.div` padding: 10px; + background-color: #002c2d; + color: white; + display: flex; + flex-wrap: nowrap; + &.selected { + background-color: #3c7173; + } + div:first-child { + width: 20%; + padding-top: 5px; + } `; export const ServerUrl = styled.div` text-align: left; - padding: 5px; - border: 1px solid #ccc; - background: #fff; + user-select: none; + padding: 5px !important; + background-color: #ffffff33; word-break: break-all; - color: ${props => props.theme.colors.primary.main}; + width: 100% !important; + color: #00ff1c; > span { - color: ${props => props.theme.colors.text.primary}; + color: white; }; `; diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index e896ed2f..c9fc8ee6 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -13,7 +13,7 @@ import { WrappedShelfIcon, } from '../../common-elements/fields-layout'; -import { ShelfIcon } from '../../common-elements/'; +import { ShelfIcon, TextField } from '../../common-elements/'; import { FieldModel } from '../../services/models'; import { Schema, SchemaOptions } from '../Schema/Schema'; @@ -33,6 +33,12 @@ export class Field extends React.Component { toggle = () => { this.props.field.toggle(); }; + + onFieldChange = e => { + console.log('Textfield value is ' + e.target.placeholder + ' - ' + e.target.value); + this.props.field.setValue(e.target.value); + }; + render() { const { className, field, isLast } = this.props; const { name, expanded, deprecated, required, kind } = field; @@ -53,12 +59,12 @@ export class Field extends React.Component { {required && required } ) : ( - - - {name} - {required && required } - - ); + + + {name} + {required && required } + + ); return ( <> @@ -67,6 +73,9 @@ export class Field extends React.Component { + {field && field.in === 'path' && + + } {field.expanded && withSubSchema && ( diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx index 544a2bc8..ec3443ef 100644 --- a/src/components/Markdown/Markdown.tsx +++ b/src/components/Markdown/Markdown.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { MarkdownRenderer } from '../../services'; +import styled from '../../styled-components'; import { SanitizedMarkdownHTML } from './SanitizedMdBlock'; export interface StylingMarkdownProps { @@ -17,19 +18,31 @@ export type MarkdownProps = BaseMarkdownProps & StylingMarkdownProps & { source: string; className?: string; + onSelectUrl?: any; }; export class Markdown extends React.Component { + handleClick = () => { + this.props.onSelectUrl(); + }; render() { const { source, inline, compact, className } = this.props; const renderer = new MarkdownRenderer(); return ( - + + + ); } } +const MarkWrapper = styled.div` + div { + width: 100% !important; + padding-top: 0 !important; + } +`; diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 1283a6ad..2e634bc3 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -1,24 +1,25 @@ -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 as OperationType, SecuritySchemesModel } from '../../services/models'; +import styled from '../../styled-components'; +import { ConsoleViewer } from '../Console/ConsoleViewer'; import { Endpoint } from '../Endpoint/Endpoint'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; -import { Markdown } from '../Markdown/Markdown'; -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 { Markdown } from '../Markdown/Markdown'; + +import {SwitchBox} from '../../common-elements/SwitchBox'; +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 {SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; const OperationRow = styled(Row)` backface-visibility: hidden; @@ -33,12 +34,34 @@ const Description = styled.div` export interface OperationProps { operation: OperationType; + securitySchemes: SecuritySchemesModel; +} + +export interface OperationState { + executeMode: boolean; + urlIndex: number; } @observer -export class Operation extends React.Component { +export class Operation extends React.Component { + + constructor(props) { + super(props); + this.state = { + executeMode: false, + urlIndex: 0, + }; + } + + onConsoleClick = () => { + this.setState({ + executeMode: !this.state.executeMode, + }); + } + render() { - const { operation } = this.props; + const { operation, securitySchemes } = this.props; + const { executeMode, urlIndex } = this.state; const { name: summary, description, deprecated, externalDocs } = operation; const hasDescription = !!(description || externalDocs); @@ -52,7 +75,14 @@ export class Operation extends React.Component { {summary} {deprecated && Deprecated } - {options.pathInMiddlePanel && } + {options.enableConsole && + + } + {options.pathInMiddlePanel && } {hasDescription && ( {description !== undefined && } @@ -65,13 +95,34 @@ export class Operation extends React.Component { - {!options.pathInMiddlePanel && } - - + {!options.pathInMiddlePanel && } + {executeMode && +
+ +
+ } + {!executeMode && + + } + {!executeMode && + + }
)} ); } + onUrlChanged = (index= 0) => { + this.setState({ + urlIndex: index, + }); + } } diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 2b43e77d..de665930 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -57,7 +57,7 @@ export class Redoc extends React.Component { - + diff --git a/src/components/Schema/Schema.tsx b/src/components/Schema/Schema.tsx index f71c33a2..2a533190 100644 --- a/src/components/Schema/Schema.tsx +++ b/src/components/Schema/Schema.tsx @@ -44,9 +44,7 @@ export class Schema extends React.Component> { if (discriminatorProp !== undefined) { if (!oneOf || !oneOf.length) { throw new Error( - `Looks like you are using discriminator wrong: you don't have any definition inherited from the ${ - schema.title - }`, + `Looks like you are using discriminator wrong: you don't have any definition inherited from the ${schema.title}`, ); } return ( @@ -66,9 +64,9 @@ export class Schema extends React.Component> { switch (type) { case 'object': - return ; + return ; case 'array': - return ; + return ; } // TODO: maybe adjust FieldDetails to accept schema diff --git a/src/components/SecurityRequirement/SecurityRequirement.tsx b/src/components/SecurityRequirement/SecurityRequirement.tsx index e6aafb30..dafb5299 100644 --- a/src/components/SecurityRequirement/SecurityRequirement.tsx +++ b/src/components/SecurityRequirement/SecurityRequirement.tsx @@ -67,18 +67,16 @@ export class SecurityRequirement extends React.PureComponent - {security.schemes.map(scheme => { - return ( - - {scheme.id} - {scheme.scopes.length > 0 && ' ('} - {scheme.scopes.map(scope => ( - {scope} - ))} - {scheme.scopes.length > 0 && ') '} - - ); - })} + {security.schemes.map(scheme => ( + + {scheme.id} + {scheme.scopes.length > 0 && ' ('} + {scheme.scopes.map(scope => ( + {scope} + ))} + {scheme.scopes.length > 0 && ') '} + + ))} ); } diff --git a/src/components/SecuritySchemes/SecuritySchemes.tsx b/src/components/SecuritySchemes/SecuritySchemes.tsx index fc5f3bc0..b05480e8 100644 --- a/src/components/SecuritySchemes/SecuritySchemes.tsx +++ b/src/components/SecuritySchemes/SecuritySchemes.tsx @@ -1,8 +1,10 @@ +import { observer } from 'mobx-react'; import * as React from 'react'; +import { TokenGroup } from '..'; + +import { DarkRightPanel, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { SecuritySchemesModel } from '../../services/models'; - -import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { OpenAPISecurityScheme } from '../../types'; import { titleize } from '../../utils/helpers'; import { Markdown } from '../Markdown/Markdown'; @@ -18,11 +20,12 @@ const AUTH_TYPES = { export interface OAuthFlowProps { type: string; flow: OpenAPISecurityScheme['flows'][keyof OpenAPISecurityScheme['flows']]; + token?: string; } export class OAuthFlow extends React.PureComponent { render() { - const { type, flow } = this.props; + const { type, flow, token } = this.props; return ( {type} OAuth Flow @@ -49,13 +52,14 @@ export class OAuthFlow extends React.PureComponent { Scopes:
    - {Object.keys(flow!.scopes).map(scope => ( + {Object.keys(flow!.scopes || {}).map(scope => (
  • {scope} -
  • ))}
+ {token} ); } @@ -65,7 +69,31 @@ export interface SecurityDefsProps { securitySchemes: SecuritySchemesModel; } -export class SecurityDefs extends React.PureComponent { +export interface SecurityDefsState { + tokens: Dict; +} + +@observer +export class SecurityDefs extends React.PureComponent { + + state = { + tokens: {}, + }; + + mutateToken = (scheme, id) => { + return () => { + scheme.setToken(this.state.tokens[id]); + }; + }; + + setToken = id => { + return token => { + const tokens = this.state.tokens; + tokens[id] = token; + this.setState({tokens}); + }; + }; + render() { return this.props.securitySchemes.schemes.map(scheme => (
@@ -82,22 +110,26 @@ export class SecurityDefs extends React.PureComponent { Security scheme type: {AUTH_TYPES[scheme.type] || scheme.type} + Value {scheme.apiKey ? ( {titleize(scheme.apiKey.in || '')} parameter name: {scheme.apiKey.name} + {scheme.token} ) : scheme.http ? ( [ HTTP Authorization Scheme {scheme.http.scheme} + {scheme.token} , scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && ( Bearer format "{scheme.http.bearerFormat}" + {scheme.token} ), ] @@ -109,16 +141,25 @@ export class SecurityDefs extends React.PureComponent { {scheme.openId.connectUrl} + {scheme.token} ) : scheme.flows ? ( Object.keys(scheme.flows).map(type => ( - + )) ) : null} + + +
)); diff --git a/src/components/SelectOnClick/SelectOnClick.tsx b/src/components/SelectOnClick/SelectOnClick.tsx index 9856905c..0f3b879b 100644 --- a/src/components/SelectOnClick/SelectOnClick.tsx +++ b/src/components/SelectOnClick/SelectOnClick.tsx @@ -1,19 +1,28 @@ import * as React from 'react'; import { ClipboardService } from '../../services'; +import styled from '../../styled-components'; -export class SelectOnClick extends React.PureComponent { +interface SelectOnClickProps { + onSelectUrl: () => void; +} + +export class SelectOnClick extends React.PureComponent { private child: HTMLDivElement | null; handleClick = () => { ClipboardService.selectElement(this.child); + this.props.onSelectUrl(); }; render() { const { children } = this.props; return ( -
(this.child = el)} onClick={this.handleClick}> + (this.child = el)} onClick={this.handleClick.bind(this, children)}> {children} -
+ ); } } +const SelectArea = styled.div` + width: 80%; +`; diff --git a/src/components/StickySidebar/StickyResponsiveSidebar.tsx b/src/components/StickySidebar/StickyResponsiveSidebar.tsx index 5a9f1817..7159dc1e 100644 --- a/src/components/StickySidebar/StickyResponsiveSidebar.tsx +++ b/src/components/StickySidebar/StickyResponsiveSidebar.tsx @@ -19,6 +19,10 @@ export interface StickySidebarProps { menu: MenuStore; } +export interface StickySidebarState { + offsetTop?: string; +} + const stickyfill = Stickyfill && Stickyfill(); const StyledStickySidebar = styled.div<{ open?: boolean }>` @@ -77,13 +81,26 @@ const FloatingButton = styled.div` `; @observer -export class StickyResponsiveSidebar extends React.Component { +export class StickyResponsiveSidebar extends React.Component< + StickySidebarProps, + StickySidebarState +> { + static contextType = OptionsContext; + context!: React.ContextType; + state: StickySidebarState = { offsetTop: '0px' }; + stickyElement: Element; componentDidMount() { if (stickyfill) { stickyfill.add(this.stickyElement); } + + // rerender when hydrating from SSR + // see https://github.com/facebook/react/issues/8017#issuecomment-256351955 + this.setState({ + offsetTop: this.getScrollYOffset(this.context), + }); } componentWillUnmount() { @@ -92,7 +109,7 @@ export class StickyResponsiveSidebar extends React.Component } } - getScrollYOffset(options) { + getScrollYOffset(options: RedocNormalizedOptions) { let top; if (this.props.scrollYOffset !== undefined) { top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)(); @@ -105,43 +122,32 @@ export class StickyResponsiveSidebar extends React.Component render() { const open = this.props.menu.sideBarOpened; - const style = options => { - const top = this.getScrollYOffset(options); - return { - top, - height: `calc(100vh - ${top})`, - }; - }; + const top = this.state.offsetTop; return ( - - {options => ( - <> - { - this.stickyElement = el as any; - }} - > - {this.props.children} - - - - - - )} - + <> + { + this.stickyElement = el as any; + }} + > + {this.props.children} + + + + + ); } private toggleNavMenu = () => { this.props.menu.toggleSidebar(); }; - - // private closeNavMenu = () => { - // this.setState({ open: false }); - // }; } diff --git a/src/components/TokenGroup/TokenGroup.tsx b/src/components/TokenGroup/TokenGroup.tsx new file mode 100644 index 00000000..fd189e31 --- /dev/null +++ b/src/components/TokenGroup/TokenGroup.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import {Button, RightPanelHeader} from '../../common-elements'; +import styled from '../../styled-components'; + +const SaveTokenButton = styled(Button)` + padding: 10px 30px; + border-radius: 0 4px 4px 0; + cursor: pointer; + text-align: center; + outline: none; + margin: 0 + min-width: 60px; + max-width: 100px; + font-weight: bold; + flex: 1 1; + order: 2; +`; + +const TokenTextField = styled.input` + padding: 10px 30px 10px 20px; + border-radius: 4px 0 0 4px; + background-color: ${props => props.theme.codeSample.backgroundColor}; + color: ${props => props.theme.codeSample.textColor} + white-space: nowrap; + align-items: center; + border: none; + direction: ltr; + min-width: 300px; + flex: 4 1; + order: 1; +`; + +const TokenGroupContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-items: stretch; + align-content: flex-start; +`; + +const Description = styled.p` + color: white; +`; + +interface TokenGroupProps { + title: string; + description?: string; + onSubmit: () => void; + onChange: (value: string) => void; +} + +export class TokenGroup extends React.PureComponent { + constructor(props) { + super(props); + this.submit = this.submit.bind(this); + this.change = this.change.bind(this); + } + + submit() { + this.props.onSubmit(); + } + + change(e) { + this.props.onChange(e.target.value); + } + + render() { + return ( + <> + + {this.props.title} + + + + Save + + + {this.props.description} + + + ); + } +} diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 745c4cad..be73390d 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -6,6 +6,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat ; private _refCounter: RefCounter = new RefCounter(); @@ -53,6 +58,8 @@ export class OpenAPIParser { this.spec = spec; + this.mergeRefs = new Set(); + const href = IS_BROWSER ? window.location.href : ''; if (typeof specUrl === 'string') { this.specUrl = urlResolve(href, specUrl); @@ -74,7 +81,10 @@ export class OpenAPIParser { ) { // Automatically inject Authentication section with SecurityDefinitions component const description = spec.info.description || ''; - if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) { + if ( + !MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME) && + !MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_JSX_NAME) + ) { const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME); spec.info.description = appendToMdHeading(description, 'Authentication', comment); } @@ -176,7 +186,12 @@ export class OpenAPIParser { schema: OpenAPISchema, $ref?: string, forceCircular: boolean = false, + used$Refs = new Set(), ): MergedOpenAPISchema { + if ($ref) { + used$Refs.add($ref); + } + schema = this.hoistOneOfs(schema); if (schema.allOf === undefined) { @@ -198,16 +213,25 @@ export class OpenAPIParser { receiver.items = { ...receiver.items }; } - const allOfSchemas = schema.allOf.map(subSchema => { - const resolved = this.deref(subSchema, forceCircular); - const subRef = subSchema.$ref || undefined; - const subMerged = this.mergeAllOf(resolved, subRef, forceCircular); - receiver.parentRefs!.push(...(subMerged.parentRefs || [])); - return { - $ref: subRef, - schema: subMerged, - }; - }); + const allOfSchemas = schema.allOf + .map(subSchema => { + if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) { + return undefined; + } + + const resolved = this.deref(subSchema, forceCircular); + const subRef = subSchema.$ref || undefined; + const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs); + receiver.parentRefs!.push(...(subMerged.parentRefs || [])); + return { + $ref: subRef, + schema: subMerged, + }; + }) + .filter(child => child !== undefined) as Array<{ + $ref: string | undefined; + schema: MergedOpenAPISchema; + }>; for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) { if ( diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 1bcffb27..3e456bb0 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -9,6 +9,8 @@ export interface RedocRawOptions { theme?: ThemeInterface; scrollYOffset?: number | string | (() => number); hideHostname?: boolean | string; + enableConsole?: boolean; + additionalHeaders?: object; expandResponses?: string | 'all'; requiredPropsFirst?: boolean | string; sortPropsAlphabetically?: boolean | string; @@ -25,12 +27,20 @@ export interface RedocRawOptions { menuToggle?: boolean | string; jsonSampleExpandLevel?: number | string | 'all'; + providedByName?: string; + providedByUri?: string; + queryParamPrefix?: string; + queryParamSuffix?: string; + unstable_ignoreMimeParameters?: boolean; allowedMdComponents?: Dict; labels?: LabelsConfigRaw; + enumSkipQuotes?: boolean | string; + + expandDefaultServerVariables?: boolean; } function argValueToBoolean(val?: string | boolean): boolean { @@ -141,11 +151,19 @@ export class RedocNormalizedOptions { menuToggle: boolean; jsonSampleExpandLevel: number; enumSkipQuotes: boolean; + enableConsole: boolean; + additionalHeaders: object; + providedByName: string; + providedByUri: string; + queryParamPrefix: string; + queryParamSuffix: string; /* tslint:disable-next-line */ unstable_ignoreMimeParameters: boolean; allowedMdComponents: Dict; + expandDefaultServerVariables: boolean; + constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { raw = { ...defaults, ...raw }; const hook = raw.theme && raw.theme.extensionsHook; @@ -177,9 +195,17 @@ export class RedocNormalizedOptions { raw.jsonSampleExpandLevel, ); this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes); + this.enableConsole = argValueToBoolean(raw.enableConsole); + this.additionalHeaders = raw.additionalHeaders || {}; + this.providedByName = raw.providedByName || 'Documentation Powered by ReDoc'; + this.providedByUri = raw.providedByUri || 'https://github.com/Rebilly/ReDoc'; + this.queryParamPrefix = raw.queryParamPrefix || '{'; + this.queryParamSuffix = raw.queryParamSuffix || '}'; this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.allowedMdComponents = raw.allowedMdComponents || {}; + + this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables); } } diff --git a/src/services/SearchStore.ts b/src/services/SearchStore.ts index 669d8d05..ac7c57b8 100644 --- a/src/services/SearchStore.ts +++ b/src/services/SearchStore.ts @@ -9,7 +9,7 @@ let worker: new () => Worker; if (IS_BROWSER) { try { // tslint:disable-next-line - worker = require('workerize-loader?inline&fallback=false!./SearchWorker.worker'); + worker = require('workerize-loader?fallback=false!./SearchWorker.worker'); } catch (e) { worker = require('./SearchWorker.worker').default; } diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts index 5302a09f..d1c17697 100644 --- a/src/services/models/Field.ts +++ b/src/services/models/Field.ts @@ -29,9 +29,8 @@ function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): Open * Field or Parameter model ready to be used by components */ export class FieldModel { - @observable - expanded: boolean = false; - + @observable expanded: boolean = false; + @observable $value: string = ''; schema: SchemaModel; name: string; required: boolean; @@ -92,4 +91,9 @@ export class FieldModel { toggle() { this.expanded = !this.expanded; } + + @action + setValue(value: string) { + this.$value = value; + } } diff --git a/src/services/models/SecuritySchemes.ts b/src/services/models/SecuritySchemes.ts index fc5deda8..2fb62f92 100644 --- a/src/services/models/SecuritySchemes.ts +++ b/src/services/models/SecuritySchemes.ts @@ -1,3 +1,4 @@ +import {observable} from 'mobx'; import { OpenAPISecurityScheme, Referenced } from '../../types'; import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils/openapi'; import { OpenAPIParser } from '../OpenAPIParser'; @@ -22,6 +23,9 @@ export class SecuritySchemeModel { connectUrl: string; }; + @observable + token?: string = ''; + constructor(parser: OpenAPIParser, id: string, scheme: Referenced) { const info = parser.deref(scheme); this.id = id; @@ -52,9 +56,14 @@ export class SecuritySchemeModel { this.flows = info.flows; } } + + setToken(token: string) { + this.token = token; + } } export class SecuritySchemesModel { + @observable schemes: SecuritySchemeModel[]; constructor(parser: OpenAPIParser) { diff --git a/src/theme.ts b/src/theme.ts index 4b9f37f2..812be850 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -149,6 +149,10 @@ const defaultTheme: ThemeInterface = { }, codeSample: { backgroundColor: ({ rightPanel }) => darken(0.1, rightPanel.backgroundColor), + textColor: ({ rightPanel }) => rightPanel.textColor, + }, + styledPre: { + maxHeight: '500px', }, }; @@ -321,9 +325,13 @@ export interface ResolvedThemeInterface { }; codeSample: { backgroundColor: string; + textColor: string; }; extensionsHook?: (name: string, props: any) => string; + styledPre: { + maxHeight: string; + }; } export type primitive = string | number | boolean | undefined | null; diff --git a/src/utils/__tests__/helpers.test.ts b/src/utils/__tests__/helpers.test.ts index e638ac12..52e3e289 100644 --- a/src/utils/__tests__/helpers.test.ts +++ b/src/utils/__tests__/helpers.test.ts @@ -60,7 +60,7 @@ describe('Utils', () => { test('should behave like Object.assign on the top level', () => { const obj1 = { a: { a1: 'A1' }, c: 'C' }; const obj2 = { a: undefined, b: { b1: 'B1' } }; - expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2)); + expect(mergeObjects({}, obj1, obj2)).toEqual({ ...obj1, ...obj2 }); }); test('should not merge array values, just override', () => { const obj1 = { a: ['A', 'B'] }; diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index 5d2d91dd..92057583 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -13,6 +13,7 @@ import { import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services'; import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types'; +import { expandDefaultServerVariables } from '../openapi'; describe('Utils', () => { describe('openapi getStatusCode', () => { @@ -293,6 +294,39 @@ describe('Utils', () => { ]); expect(res).toEqual([{ url: 'https://base.com/sandbox/test', description: 'test' }]); }); + + it('should expand variables', () => { + const servers = normalizeServers('', [ + { + url: 'http://{host}{basePath}', + variables: { + host: { + default: '127.0.0.1', + }, + basePath: { + default: '/path/to/endpoint', + }, + }, + }, + { + url: 'http://127.0.0.2:{port}', + variables: {}, + }, + { + url: 'http://127.0.0.3', + }, + ]); + + expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual( + 'http://127.0.0.1/path/to/endpoint', + ); + expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual( + 'http://127.0.0.2:{port}', + ); + expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual( + 'http://127.0.0.3', + ); + }); }); describe('openapi humanizeConstraints', () => { @@ -404,7 +438,7 @@ describe('Utils', () => { { style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' }, { style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' }, { style: 'label', explode: false, expected: '.role,admin,firstName,Alex' }, - { style: 'label', explode: true, expected: '.role=admin,firstName=Alex' }, + { style: 'label', explode: true, expected: '.role=admin.firstName=Alex' }, { style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' }, { style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' }, ], @@ -516,9 +550,7 @@ describe('Utils', () => { locationTestGroup.cases.forEach(valueTypeTestGroup => { describe(valueTypeTestGroup.description, () => { valueTypeTestGroup.cases.forEach(testCase => { - it(`should serialize correctly when style is ${testCase.style} and explode is ${ - testCase.explode - }`, () => { + it(`should serialize correctly when style is ${testCase.style} and explode is ${testCase.explode}`, () => { const parameter: OpenAPIParameter = { name: locationTestGroup.name, in: locationTestGroup.location, diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 00000000..b332d125 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1 @@ +//parseparseFetchFetch \ No newline at end of file diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index cb90dc97..b491340e 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -83,7 +83,7 @@ export function appendToMdHeading(md: string, heading: string, content: string) } // credits https://stackoverflow.com/a/46973278/1749888 -export const mergeObjects = (target: T, ...sources: T[]): T => { +export const mergeObjects = (target: any, ...sources: any[]): any => { if (!sources.length) { return target; } diff --git a/src/utils/loadAndBundleSpec.ts b/src/utils/loadAndBundleSpec.ts index d2431b8d..e8d3532d 100644 --- a/src/utils/loadAndBundleSpec.ts +++ b/src/utils/loadAndBundleSpec.ts @@ -9,11 +9,20 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi resolve: { http: { withCredentials: false } }, } as object)) as any; + let v2Specs = spec; if (spec.swagger !== undefined) { - return convertSwagger2OpenAPI(spec); - } else { - return spec; + v2Specs = await convertSwagger2OpenAPI(spec); } + + // we can derefrence the schema here for future use. + // import { cloneDeep } from 'lodash'; + // const derefrencedSpec = await parser.dereference(cloneDeep(spec)); + // const derefed = await parser.dereference(v2Specs, { + // resolve: { http: { withCredentials: false } }, + // } as object); + + return v2Specs; + } export function convertSwagger2OpenAPI(spec: any): Promise { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 04a16b6b..7eefb30d 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -1,5 +1,5 @@ import { dirname } from 'path'; -import { URI } from 'uri-template-lite'; +const URLtemplate = require('url-template'); import { OpenAPIParser } from '../services/OpenAPIParser'; import { @@ -168,7 +168,7 @@ function serializeFormValue(name: string, explode: boolean, value: any) { // e.g. URI.template doesn't parse names with hypen (-) which are valid query param names const safeName = '__redoc_param_name__'; const suffix = explode ? '*' : ''; - const template = new URI.Template(`{?${safeName}${suffix}}`); + const template = URLtemplate.parse(`{?${safeName}${suffix}}`); return template .expand({ [safeName]: value }) .substring(1) @@ -227,7 +227,7 @@ function serializePathParameter( // Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later // e.g. URI.template doesn't parse names with hypen (-) which are valid query param names const safeName = '__redoc_param_name__'; - const template = new URI.Template(`{${prefix}${safeName}${suffix}}`); + const template = URLtemplate.parse(`{${prefix}${safeName}${suffix}}`); return template.expand({ [safeName]: value }).replace(/__redoc_param_name__/g, name); } @@ -263,7 +263,7 @@ function serializeQueryParameter( return `${name}=${value.join('|')}`; case 'deepObject': if (!explode || Array.isArray(value) || typeof value !== 'object') { - console.warn('The style deepObject is only applicable for objects with expolde=true'); + console.warn('The style deepObject is only applicable for objects with explode=true'); return ''; } @@ -285,7 +285,7 @@ function serializeHeaderParameter( // name is not important here, so use RFC6570 safe name ([a-zA-Z0-9_]) const name = '__redoc_param_name__'; - const template = new URI.Template(`{${name}${suffix}}`); + const template = URLtemplate.parse(`{${name}${suffix}}`); return decodeURIComponent(template.expand({ [name]: value })); default: console.warn('Unexpected style for header: ' + style); @@ -487,6 +487,13 @@ export function mergeSimilarMediaTypes(types: Dict): Dict (variables[name] && variables[name].default) || match, + ); +} + export function normalizeServers( specUrl: string | undefined, servers: OpenAPIServer[], diff --git a/tsconfig.json b/tsconfig.json index 6a327741..466ea081 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,4 +39,4 @@ "./src/**/*.ts?", "demo/*.tsx" ] -} +} \ No newline at end of file diff --git a/tslint.json b/tslint.json index 9c980dcb..feec7e40 100644 --- a/tslint.json +++ b/tslint.json @@ -17,7 +17,7 @@ "quotemark": [true, "single", "avoid-template", "jsx-double"], "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], "arrow-parens": [true, "ban-single-arg-parens"], - "no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js"], + "no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js", "brace"], "object-literal-key-quotes": [true, "as-needed"], "no-unused-expression": [true, "allow-tagged-template"], "semicolon": [true, "always", "ignore-bound-class-methods"], diff --git a/yarn.lock b/yarn.lock index 04ee8e94..2203608b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -570,6 +570,14 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" +"@babel/polyfill@^7.4.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.8.3.tgz#2333fc2144a542a7c07da39502ceeeb3abe4debd" + integrity sha512-0QEgn2zkCzqGIkSWWAEmvxD7e00Nm9asTtQvi7HdlYvMhjy/J38V/1Y9ode0zEJeIuxAI0uftiAzqc7nVeWUGg== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" + "@babel/preset-env@^7.0.0": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" @@ -1052,11 +1060,21 @@ resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.0.tgz#4328c9f65698e59f4feade8f4e5d928c748fd643" integrity sha512-mEyuziLrfDCQ4juQP1k706BUU/c8OGn/ZFl69AXXY6dStHClKX4P+N8+rhqpul1vRDA2VOygzMRSJJZHyDEOfw== +"@types/promise@^7.1.30": + version "7.1.30" + resolved "https://registry.yarnpkg.com/@types/promise/-/promise-7.1.30.tgz#1b6714b321fdfc54d1527e7a17116a0e1f2ab810" + integrity sha1-G2cUsyH9/FTRUn56FxFqDh8quBA= + "@types/prop-types@*", "@types/prop-types@^15.7.1": version "15.7.1" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== +"@types/qs@^6.5.1": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.0.tgz#2a5fa918786d07d3725726f7f650527e1cfeaffd" + integrity sha512-c4zji5CjWv1tJxIZkz1oUtGcdOlsH3aza28Nqmm+uNDWBRHoMsjooBEN4czZp1V3iXPihE/VRUOBqg+4Xq0W4g== + "@types/react-dom@^16.8.5": version "16.8.5" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.5.tgz#3e3f4d99199391a7fb40aa3a155c8dd99b899cbd" @@ -1459,6 +1477,16 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.4.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -1953,6 +1981,11 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" + integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= + braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -2879,6 +2912,11 @@ core-js@^2.5.7: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== +core-js@^2.6.5: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + core-js@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07" @@ -3323,6 +3361,11 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +diff-match-patch@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" + integrity sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg== + diff-sequences@^24.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975" @@ -4022,6 +4065,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -6205,6 +6253,11 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -7886,6 +7939,11 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.5.2: + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -7976,6 +8034,18 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-ace@^6.0.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-6.6.0.tgz#a79457ef03c3b1f8d4fc598a003b1d6ad464f1a0" + integrity sha512-Jehhp8bxa8kqiXk07Jzy+uD5qZMBwo43O+raniGHjdX7Qk93xFkKaAz8LxtUVZPJGlRnV5ODMNj0qHwDSN+PBw== + dependencies: + "@babel/polyfill" "^7.4.4" + brace "^0.11.1" + diff-match-patch "^1.0.4" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + prop-types "^15.7.2" + react-dom@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" @@ -8017,6 +8087,13 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-switch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-switch/-/react-switch-5.0.1.tgz#449277f4c3aed5286fffd0f50d5cbc2a23330406" + integrity sha512-Pa5kvqRfX85QUCK1Jv0rxyeElbC3aNpCP5hV0LoJpU/Y6kydf0t4kRriQ6ZYA4kxWwAYk/cH51T4/sPzV9mCgQ== + dependencies: + prop-types "^15.6.2" + react-tabs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.0.0.tgz#60311a17c755eb6aa9b3310123e67db421605127" @@ -9846,6 +9923,11 @@ url-polyfill@^1.1.7: resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.7.tgz#402ee84360eb549bbeb585f4c7971e79a31de9e3" integrity sha512-ZrAxYWCREjmMtL8gSbSiKKLZZticgihCvVBtrFbUVpyoETt8GQJeG2okMWA8XryDAaHMjJfhnc+rnhXRbI4DXA== +url-template@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" + integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= + url@0.11.0, url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"