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/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 bd8e887f..07cc2117 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/package.json b/package.json index a0ec70b3..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", @@ -135,10 +135,10 @@ "styled-components": "^4.1.1" }, "dependencies": { - "classnames": "^2.2.6", "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", "eventemitter3": "^4.0.0", @@ -154,16 +154,18 @@ "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-tabs": "^3.0.0", - "slugify": "^1.3.4", "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 index a673ffd7..bbdc6524 100644 --- a/src/common-elements/buttons.ts +++ b/src/common-elements/buttons.ts @@ -9,12 +9,15 @@ export const Button = styled.button` padding: 5px; `; -export const SendButton = styled(Button)` - background: #B0045E; -`; - -export const ConsoleButton = styled(Button)` - background: #e2e2e2; - color: black; - float: right; +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/panels.ts b/src/common-elements/panels.ts index cda0b7e3..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`}; `}; `; @@ -80,10 +82,6 @@ export const FlexLayout = styled.div` width: 100%; `; -export const ConsoleActionsRow = styled(FlexLayout)` - padding: 5px 0px; -`; - export const FlexLayoutReverse = styled(FlexLayout)` flex-direction: row-reverse; `; diff --git a/src/components/Console/ConsoleEditor.tsx b/src/components/Console/ConsoleEditor.tsx index d68c0f48..3726d8ec 100644 --- a/src/components/Console/ConsoleEditor.tsx +++ b/src/components/Console/ConsoleEditor.tsx @@ -8,7 +8,8 @@ import 'brace/mode/json'; import 'brace/theme/github'; import 'brace/theme/monokai'; -import { MediaTypeModel } from '../../services/models'; +import {MediaTypeModel} from '../../services/models'; +import {ConsoleEditorWrapper} from './ConsoleEditorWrapper'; export interface ConsoleEditorProps { mediaTypes: MediaTypeModel[]; @@ -37,7 +38,7 @@ export class ConsoleEditor extends React.Component { } return ( -
+ { showLineNumbers: true, tabSize: 2, }} - fontSize={10} + fontSize={15} mode="json" - theme="monokai" name="request-builder-editor" editorProps={{ $blockScrolling: true }} value={JSON.stringify(sample, null, 2)} ref={(ace: AceEditor) => (this.editor = ace)} - width="100%" - height="400px" /> -
+ ); } diff --git a/src/components/Console/ConsoleEditorWrapper.ts b/src/components/Console/ConsoleEditorWrapper.ts new file mode 100644 index 00000000..85152e1d --- /dev/null +++ b/src/components/Console/ConsoleEditorWrapper.ts @@ -0,0 +1,75 @@ +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 { + background: ${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; + } + & .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 index 200e4073..0fa4822c 100644 --- a/src/components/Console/ConsoleViewer.tsx +++ b/src/components/Console/ConsoleViewer.tsx @@ -1,7 +1,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; -import { SendButton } from '../../common-elements/buttons'; -import { ConsoleActionsRow } from '../../common-elements/panels'; +import { SubmitButton } from '../../common-elements/buttons'; +import { FlexLayoutReverse } from '../../common-elements/panels'; import { FieldModel, OperationModel } from '../../services/models'; import { OpenAPISchema } from '../../types'; import { SourceCodeWithCopy } from '../SourceCode/SourceCode'; @@ -30,7 +30,7 @@ export class ConsoleViewer extends React.Component -

Console

+

Request

{hasBodySample && ( (this.consoleEditor = editor)} + ref={(editor: any) => (this.consoleEditor = editor)} /> )} - {false && samples.map(sample => ( - - ))} - - Send Request - + + Send Request + {result && } diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 3b638896..01f76eb5 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -60,7 +60,7 @@ export class ContentItem extends React.Component { } } -const middlePanelWrap = component => {component}; +const middlePanelWrap = component => {component}; @observer export class SectionItem extends React.Component { @@ -71,7 +71,7 @@ export class SectionItem extends React.Component { return ( <> - +
{name} diff --git a/src/components/Endpoint/Endpoint.tsx b/src/components/Endpoint/Endpoint.tsx index 21bffa52..280a8582 100644 --- a/src/components/Endpoint/Endpoint.tsx +++ b/src/components/Endpoint/Endpoint.tsx @@ -5,7 +5,7 @@ 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, @@ -61,15 +61,18 @@ export class Endpoint extends React.Component { {operation.servers.map(server => { + 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} diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index ef6bc251..d31b5e15 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -1,25 +1,25 @@ -import * as React from 'react'; -import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; - import { observer } from 'mobx-react'; +import * as React from 'react'; -import { Badge, ConsoleButton, DarkRightPanel, FlexLayoutReverse, H2, MiddlePanel, Row } from '../../common-elements'; - -import { OptionsContext } from '../OptionsProvider'; +import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements'; import { ShareLink } from '../../common-elements/linkify'; -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 { ConsoleViewer } from '../Console/ConsoleViewer'; +import { Endpoint } from '../Endpoint/Endpoint'; +import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; 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; @@ -62,7 +62,6 @@ export class Operation extends React.Component { const { name: summary, description, deprecated, externalDocs } = operation; const hasDescription = !!(description || externalDocs); - const consoleButtonLabel = (executeMode) ? 'Hide Console' : 'Show Console'; return ( @@ -74,9 +73,11 @@ export class Operation extends React.Component { {summary} {deprecated && Deprecated } {options.enableConsole && - - {consoleButtonLabel} - + } {options.pathInMiddlePanel && } {hasDescription && ( 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/SecuritySchemes/SecuritySchemes.tsx b/src/components/SecuritySchemes/SecuritySchemes.tsx index ebae4e44..b05480e8 100644 --- a/src/components/SecuritySchemes/SecuritySchemes.tsx +++ b/src/components/SecuritySchemes/SecuritySchemes.tsx @@ -52,7 +52,7 @@ export class OAuthFlow extends React.PureComponent { Scopes:
    - {Object.keys(flow!.scopes).map(scope => ( + {Object.keys(flow!.scopes || {}).map(scope => (
  • {scope} -
  • 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/__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 668cd7de..3e456bb0 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -37,7 +37,10 @@ export interface RedocRawOptions { allowedMdComponents?: Dict; labels?: LabelsConfigRaw; + enumSkipQuotes?: boolean | string; + + expandDefaultServerVariables?: boolean; } function argValueToBoolean(val?: string | boolean): boolean { @@ -159,6 +162,8 @@ export class RedocNormalizedOptions { unstable_ignoreMimeParameters: boolean; allowedMdComponents: Dict; + expandDefaultServerVariables: boolean; + constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { raw = { ...defaults, ...raw }; const hook = raw.theme && raw.theme.extensionsHook; @@ -200,5 +205,7 @@ export class RedocNormalizedOptions { this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.allowedMdComponents = raw.allowedMdComponents || {}; + + this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables); } } 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/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/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/yarn.lock b/yarn.lock index 3c0db313..2203608b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1467,7 +1467,7 @@ ajv@^5.5.2: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.4.0, ajv@^6.5.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== @@ -1477,6 +1477,16 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.4.0, 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" @@ -4055,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" @@ -8072,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" @@ -9901,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"