diff --git a/.eslintignore b/.eslintignore index dcf11353ec..2f59b98bca 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ **/build/** **/.git/** **/public/** +**/microsite/** diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md index 012b8d7a06..d70622bf52 100644 --- a/.github/ISSUE_TEMPLATE/feature_template.md +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -1,7 +1,7 @@ --- name: 'Feature Request' about: 'Suggest new features and changes' -labels: help wanted +labels: enhancement --- diff --git a/.github/workflows/chromatic-storybook-test.yml b/.github/workflows/chromatic-storybook-test.yml index e38b494a73..0bcaf400d0 100644 --- a/.github/workflows/chromatic-storybook-test.yml +++ b/.github/workflows/chromatic-storybook-test.yml @@ -14,7 +14,37 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 # Required to retrieve git history - - run: yarn install && yarn build-storybook + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + + - run: yarn build-storybook + - uses: chromaui/action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/ci.yml similarity index 60% rename from .github/workflows/frontend.yml rename to .github/workflows/ci.yml index 0137e4b9dc..492b008431 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,11 @@ -name: Frontend CI +name: CI on: pull_request: - + paths-ignore: + - 'microsite/**' jobs: - build: + verify: runs-on: ubuntu-latest strategy: @@ -19,33 +20,54 @@ jobs: - uses: actions/checkout@v2 - name: fetch branch master run: git fetch origin master - - name: find location of global yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v2 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: cache node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + + # Beginning of yarn setup, keep in sync between all workflows. + # TODO(Rugvip): move this to composite action once all features we use are supported - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + + # Cache every node_modules folder inside the monorepo + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + # We use both yarn.lock and package.json as cache keys to ensure that + # changes to local monorepo packages bust the cache. + key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + + # If we get a cache hit for node_modules, there's no need to bring in the global + # yarn cache or run yarn install, as all dependencies will be installed already. + + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: yarn install + if: steps.cache-modules.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + # End of yarn setup - name: check for yarn.lock changes id: yarn-lock run: git diff --quiet origin/master HEAD -- yarn.lock continue-on-error: true - - name: yarn install - run: yarn install --frozen-lockfile + - name: verify doc links + run: node docs/verify-links.js - name: lint run: yarn lerna -- run lint --since origin/master @@ -55,8 +77,7 @@ jobs: - name: build changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} - # Need to build all dependencies as well to be able to run tests later - run: yarn lerna -- run build --since origin/master --include-dependencies + run: yarn lerna -- run build --since origin/master - name: build all packages if: ${{ steps.yarn-lock.outcome == 'failure' }} @@ -75,6 +96,3 @@ jobs: - name: verify plugin template run: yarn lerna -- run diff -- --check - - - name: verify storybook - run: yarn workspace storybook build-storybook diff --git a/.github/workflows/cli-win.yml b/.github/workflows/e2e-win.yml similarity index 66% rename from .github/workflows/cli-win.yml rename to .github/workflows/e2e-win.yml index 4e7a186282..5ebf67bc9a 100644 --- a/.github/workflows/cli-win.yml +++ b/.github/workflows/e2e-win.yml @@ -1,12 +1,13 @@ -name: CLI Test Windows +name: E2E Test Windows -# Building on windows is really slow, so this workflow is separate from cli.yml and only builds on changes +# Building on windows is really slow, so this workflow is separate from e2e.yml and only builds on changes # to the cli itself. They're more likely to introduce issues on windows, compared to changes to core and yarn.lock. on: pull_request: paths: - - '.github/workflows/cli-win.yml' + - '.github/workflows/e2e-win.yml' - 'packages/cli/**' + - 'packages/e2e/**' - 'packages/create-app/**' jobs: @@ -25,6 +26,13 @@ jobs: name: Node ${{ matrix.node-version }} on ${{ matrix.os }} steps: - uses: actions/checkout@v2 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth - name: find location of global yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" @@ -32,16 +40,15 @@ jobs: uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - name: yarn install run: yarn install --frozen-lockfile + # End of yarn setup + - run: yarn tsc - - run: yarn build - - name: verify app and plugin creation - run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js + - name: yarn build + run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite + - name: run E2E test + run: yarn workspace e2e-test start diff --git a/.github/workflows/cli.yml b/.github/workflows/e2e.yml similarity index 64% rename from .github/workflows/cli.yml rename to .github/workflows/e2e.yml index 6e4fdad214..401fe7297a 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/e2e.yml @@ -1,14 +1,9 @@ -name: CLI Test +name: E2E Test Linux on: pull_request: - paths: - - '.github/workflows/cli.yml' - - 'packages/cli/**' - - 'packages/create-app/**' - - 'packages/core/**' - - 'packages/core-api/**' - - 'yarn.lock' + paths-ignore: + - 'microsite/**' jobs: build: @@ -38,30 +33,44 @@ jobs: name: Node ${{ matrix.node-version }} on ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - name: find location of global yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v2 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- - name: yarn install run: yarn install --frozen-lockfile + # End of yarn setup + - run: yarn tsc - - run: yarn build - - name: verify app and plugin creation + - name: yarn build + run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite + - name: run E2E test + run: | + sudo sysctl fs.inotify.max_user_watches=524288 + yarn workspace e2e-test start env: POSTGRES_HOST: localhost POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - run: | - sudo sysctl fs.inotify.max_user_watches=524288 - node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js diff --git a/.github/workflows/master-win.yml b/.github/workflows/master-win.yml new file mode 100644 index 0000000000..296a7511bc --- /dev/null +++ b/.github/workflows/master-win.yml @@ -0,0 +1,45 @@ +name: Master Build Windows + +on: + push: + branches: [master] + +jobs: + build: + runs-on: windows-latest + + strategy: + matrix: + node-version: [12.x] + + env: + CI: true + NODE_OPTIONS: --max-old-space-size=4096 + + steps: + - uses: actions/checkout@v2 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: find location of global yarn cache + id: yarn-cache + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + + +# Tests are broken on Windows, disabled for now + # - name: test + # run: yarn lerna -- run test diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 3ef0c1fee3..354838037e 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -1,4 +1,4 @@ -name: Master Build +name: Main Master Build on: push: @@ -18,29 +18,34 @@ jobs: steps: - uses: actions/checkout@v2 - - name: find location of global yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v2 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: cache node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- - name: yarn install run: yarn install --frozen-lockfile + # End of yarn setup - name: lint run: yarn lerna -- run lint @@ -69,3 +74,11 @@ jobs: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" package_root: "packages/core" tag_prefix: "v" + + - name: Discord notification + if: ${{ failure() }} + uses: Ilshidur/action-discord@0.2.0 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + with: + args: 'Master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}' diff --git a/.github/workflows/storybook-deploy.yml b/.github/workflows/microsite-build-check.yml similarity index 55% rename from .github/workflows/storybook-deploy.yml rename to .github/workflows/microsite-build-check.yml index 33ffba5ccd..dc86ca2630 100644 --- a/.github/workflows/storybook-deploy.yml +++ b/.github/workflows/microsite-build-check.yml @@ -1,16 +1,14 @@ -name: Deploy Storybook +name: Build microsite on: - push: - branches: - - master + pull_request: paths: - - '.github/workflows/storybook-deploy.yml' - - 'packages/storybook/**' - - 'packages/core/src/**' + - '.github/workflows/microsite-build-check.yml' + - 'microsite/**' + - 'docs/**' jobs: - deploy-storybook: + build-microsite: runs-on: ubuntu-latest strategy: @@ -23,34 +21,34 @@ jobs: steps: - uses: actions/checkout@v2 - - name: find location of global yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v2 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: cache node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- - name: yarn install run: yarn install --frozen-lockfile - - name: build storybook - run: yarn workspace storybook build-storybook - - name: deploy storybook to gh-pages - uses: JamesIves/github-pages-deploy-action@3.4.2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: packages/storybook/dist + # End of yarn setup + + - name: build microsite + run: yarn workspace backstage-microsite build diff --git a/.github/workflows/microsite-with-storybook-deploy.yml b/.github/workflows/microsite-with-storybook-deploy.yml new file mode 100644 index 0000000000..2c8b8af40c --- /dev/null +++ b/.github/workflows/microsite-with-storybook-deploy.yml @@ -0,0 +1,74 @@ +name: Deploy Microsite and Storybook + +on: + push: + branches: + - master + paths: + - '.github/workflows/microsite-with-storybook-deploy.yml' + - 'packages/storybook/**' + - 'packages/core/src/**' + - 'microsite/**' + - 'docs/**' + +jobs: + deploy-microsite-and-storybook: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [12.x] + + env: + CI: true + NODE_OPTIONS: --max-old-space-size=4096 + + steps: + - uses: actions/checkout@v2 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: build microsite + run: yarn workspace backstage-microsite build + + - name: build storybook + run: yarn workspace storybook build-storybook + + - name: move storybook dist into microsite + run: mv packages/storybook/dist/ microsite/build/backstage/storybook + + - name: Check the build output + run: ls microsite/build/backstage && ls microsite/build/backstage/storybook + + - name: Deploy both microsite and storybook to gh-pages + uses: JamesIves/github-pages-deploy-action@3.4.2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: microsite/build/backstage diff --git a/.gitignore b/.gitignore index 9884edf96e..aee80f4c10 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,10 @@ typings/ .nuxt dist +# Microsite build output +microsite/build +microsite/i18n + # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js diff --git a/ADOPTERS.md b/ADOPTERS.md index 7a96198529..0ba3e3196b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,13 +1,15 @@ -| Organization | Contact | Description of Use | -| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| Organization | Contact | Description of Use | +| -------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c67eae825..20ae9a414d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,33 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re > Collect changes for the next release below +- The backend plugin + [service builder](https://github.com/spotify/backstage/blob/master/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts) + no longer adds `express.json()` automatically to all routes. While convenient + in a lot of cases, it also led to problems where for example the proxy + middleware could hang because the body had already been altered and could not + be streamed. Also, plugins that rather wanted to handle e.g. form encoded data + still had to cater to that manually. We therefore decided to let plugins add + `express.json()` themselves if they happen to deal with JSON data. + +## v0.1.1-alpha.20 + +- Includes https://github.com/spotify/backstage/pull/2097 to resolve issues with create-plugin command. + +## v0.1.1-alpha.19 + +### @backstage/create-app + +- Many plugins have been added to the catalog and will for now be required to be added to separate apps as well. This will be solved as [#1536](https://github.com/spotify/backstage/issues/1536) gets sorted out, but for now you may need to install some plugins just to get pages to work. + +### @backstage/catalog-backend + +- Added the possibility to add static locations via `app-config.yaml`. This changed the signature of `new LocationReaders(logger)` inside `packages/backend/src/plugins/catalog.ts` to `new LocationReaders({config, logger})`. [#1890](https://github.com/spotify/backstage/pull/1890) + +### @backstage/theme + +- Changed the type signature of the palette, removing `sidebar: string` and adding `navigation: { background: string; indicator: string}`. [#1880](https://github.com/spotify/backstage/pull/1880) + ## v0.1.1-alpha.18 ### @backstage/catalog-backend diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf7622d692..853332f74d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing +# Contributing to Backstage Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. @@ -28,7 +28,7 @@ What kind of plugins should/could be created? Some inspiration from the 120+ plu ## Suggesting a plugin -If you start developing a plugin that you aim to release as open source, we suggest that you create a new [new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md). This helps the community know what plugins are in development. +If you start developing a plugin that you aim to release as open source, we suggest that you create a new [new Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development. You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work. @@ -38,7 +38,7 @@ The current documentation is very limited. Help us make the `/docs` folder come ## Contribute to Storybook -We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://storybook.backstage.io). +We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://backstage.io/storybook). Either help us [create new components](https://github.com/spotify/backstage/labels/help%20wanted) or improve stories for the existing ones (look for files with `*.stories.tsx`). @@ -60,7 +60,35 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) # Get Started! -So...feel ready to jump in? Let's do this. Head over to the [Getting Started guide](https://github.com/spotify/backstage#getting-started) 👏🏻💯 +So...feel ready to jump in? Let's do this. 👏🏻💯 + +To run a Backstage app, you will need to have the following installed: + +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12 +- [yarn](https://classic.yarnpkg.com/en/docs/install) + +After cloning this repo, open a terminal window and start the example app using the following commands from the project root: + +```bash +yarn install # Install dependencies + +yarn start # Start dev server, use --check to enable linting and type-checks +``` + +The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. + +Depending on the work you're doing, you often also want to run the example backend. Start the backend in a separate terminal session using the following: + +```bash +cd packages/backend + +yarn start + +yarn lerna run mock-data # Populate the backend with mock data +``` + +And that's it! You are good to go 👍 If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). diff --git a/README.md b/README.md index d401650a66..4009864edc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![headline](docs/headline.png) +![headline](docs/assets/headline.png) # [Backstage](https://backstage.io) @@ -10,101 +10,69 @@ ## What is Backstage? -[Backstage](https://backstage.io/) is an open platform for building developer portals. It’s based on the developer portal we’ve been using internally at Spotify for over four years. Backstage can be as simple as a services catalog or as powerful as the UX layer for your entire tech infrastructure. +[Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure. So your product teams can ship high-quality code quickly — without compromising autonomy. -For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX). - -### Features - -- Create and manage all of your organization’s software and microservices in one place. -- Services catalog keeps track of all software and its ownership. -- Visualizations provide information about your backend services and tooling, and help you monitor them. -- A unified method for managing microservices offers both visibility and control. -- Preset templates allow engineers to quickly create microservices in a standardized way ([coming soon](https://github.com/spotify/backstage/milestone/11)). -- Centralized, full-featured technical documentation with integrated tooling that makes it easy for developers to set up, publish, and maintain alongside their code ([coming soon](https://github.com/spotify/backstage/milestone/15)). - -### Benefits - -- For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification. -- For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation. -- For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones. -- For _everyone_, it’s a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. - -## Backstage Service Catalog (alpha) - -The Backstage Service Catalog — actually, a software catalog, since it includes more than just services — is a centralized system that keeps track of ownership and metadata for all the software in your ecosystem (services, websites, libraries, data pipelines, etc). The catalog is built around the concept of [metadata yaml files](https://github.com/spotify/backstage/blob/master/docs/architecture-decisions/adr002-default-catalog-file-format.md#format) stored together with the code, which are then harvested and visualized in Backstage. +Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end. ![service-catalog](https://backstage.io/blog/assets/6/header.png) -We have also found that the service catalog is a great way to organise the infrastructure tools you use to manage the software as well. This is how Backstage creates one developer portal for all your tools. Rather than asking teams to jump between different infrastructure UI’s (and incurring additional cognitive overhead each time they make a context switch), most of these tools can be organised around the entities in the catalog. +Out of the box, Backstage includes: + +- [Backstage Service Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview) for managing all your software (microservices, libraries, data pipelines, websites, ML models, etc.) +- [Backstage Software Templates](https://backstage.io/docs/features/software-templates/software-templates-index) for quickly spinning up new projects and standardizing your tooling with your organization’s best practices +- [Backstage TechDocs](https://backstage.io/docs/features/techdocs/techdocs-overview) for making it easy to create, maintain, find, and use technical documentation, using a "docs like code" approach +- Plus, a growing ecosystem of [open source plugins](https://github.com/spotify/backstage/tree/master/plugins) that further expand Backstage’s customizability and functionality + +For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX). ## Project roadmap -We created Backstage about 4 years ago. While our internal version of Backstage has had the benefit of time to mature and evolve, the first iteration of our open source version is still nascent. We are envisioning three phases of the project and we have already begun work on various aspects of these phases: +A detailed project roadmap, including already delivered milestones, is available [here](https://backstage.io/docs/overview/roadmap). -- 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to easily create a single consistent UI layer for your internal infrastructure and tools. A set of reusable [UX patterns and components](http://storybook.backstage.io) help ensure a consistent experience between tools. +## Getting Started -- 🐢 **Phase 2:** Service Catalog ([alpha released](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)) - With a single catalog, Backstage makes it easy for a team to manage ten services — and makes it possible for your company to manage thousands of them. Developers can get a uniform overview of all their software and related resources, regardless of how and where they are running, as well as an easy way to onboard and manage those resources. +There are two different ways to get started with Backstage, either by creating a standalone app, or by cloning this repo. Which method you use depends on what you're planning to do. -- 🐇 **Phase 3:** Ecosystem (later) - Everyone's infrastructure stack is different. By fostering a vibrant community of contributors we hope to provide an ecosystem of Open Source plugins/integrations that allows you to pick the tools that match your stack. +Creating a standalone instance makes it simpler to customize the application for your needs whilst staying up to date with the project. You will also depend on `@backstage` packages from NPM, making the project much smaller. This is the recommended approach if you want to kick the tyres of Backstage or setup your own instance. -Check out our [Milestones](https://github.com/spotify/backstage/milestones) and open [RFCs](https://github.com/spotify/backstage/labels/rfc) how they relate to the three Phases outlined above. +On the other hand, if you want to contribute plugins or to the project in general, it's easier to fork and clone this project. That will let you stay up to date with the latest changes, and gives you an easier path to make Pull Requests towards this repo. -Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. If this sounds interesting or you'd like to help us shape our product vision, we'd love to talk. You can email me directly: [alund@spotify.com](mailto:alund@spotify.com). +### Creating a Standalone App -## Overview +Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have +[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed +(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You will also need to have [Docker](https://docs.docker.com/engine/install/) installed to use some features like Software Templates and TechDocs. -The Backstage platform consists of a number of different components: - -- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/getting-started/create-an-app.md). -- [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_. -- [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph. -- [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins. -- **identity** - A backend service that holds your organisation's metadata. - -## Getting started - -To run a Backstage app, you will need to have the following installed: - -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12 -- [yarn](https://classic.yarnpkg.com/en/docs/install) - -After cloning this repo, open a terminal window and start the example app using the following commands from the project root: +Using `npx` you can then run the following to create an app in a chosen subdirectory of your current working directory: ```bash -yarn install # Install dependencies - -yarn start # Start dev server, use --check to enable linting and type-checks +npx @backstage/create-app ``` -The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. +You will be taken through a wizard to create your app, and the output should look something like this. You can read more about this process [here](https://backstage.io/docs/getting-started/create-an-app) -And that's it! You are good to go 👍 +### Contributing to Backstage -### Next step +You can read more in our [CONTRIBUTING.md](./CONTRIBUTING.md#get-started) guide, which can help you get setup with a Backstage development environment. -Take a look at the [Getting Started](docs/getting-started/index.md) guide to learn how to set up Backstage, and how to develop on the platform. +### Next steps + +Take a look at the [Getting Started](https://backstage.io/docs/getting-started/index) guide to learn how to set up Backstage, and how to develop on the platform. ## Documentation -- [Main documentation](docs/README.md) -- [Service Catalog](docs/features/software-catalog/index.md) -- [Create a Backstage App](docs/getting-started/create-an-app.md) -- [Architecture](docs/overview/architecture-terminology.md) ([Decisions](docs/architecture-decisions/index.md)) -- [Designing for Backstage](docs/dls/design.md) -- [Storybook - UI components](http://storybook.backstage.io) - -## Contributing - -We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.md) for more information. +- [Main documentation](https://backstage.io/docs) +- [Service Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview) +- [Architecture](https://backstage.io/docs/overview/architecture-terminology) ([Decisions](https://backstage.io/docs/architecture-decisions/adrs-overview)) +- [Designing for Backstage](https://backstage.io/docs/dls/design) +- [Storybook - UI components](https://backstage.io/storybook) ## Community - [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project - [Good First Issues](https://github.com/spotify/backstage/contribute) - Start here if you want to contribute - [RFCs](https://github.com/spotify/backstage/labels/rfc) - Help shape the technical direction -- [FAQ](docs/FAQ.md) - Frequently Asked Questions +- [FAQ](https://backstage.io/docs/FAQ) - Frequently Asked Questions - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates diff --git a/app-config.yaml b/app-config.yaml index d6c745a6c2..7eb6023f61 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -4,18 +4,25 @@ app: backend: baseUrl: http://localhost:7000 - listen: 0.0.0.0:7000 + listen: + port: 7000 cors: origin: http://localhost:3000 methods: [GET, POST, PUT, DELETE] credentials: true + database: + client: sqlite3 + connection: ':memory:' +# See README.md in the proxy-backend plugin for information on the configuration format proxy: - '/circleci/api': - target: 'https://circleci.com/api/v1.1' - changeOrigin: true - pathRewrite: - '^/proxy/circleci/api/': '/' + '/circleci/api': https://circleci.com/api/v1.1 + '/jenkins/api': + target: http://localhost:8080 + headers: + Authorization: + $secret: + env: JENKINS_BASIC_AUTH_HEADER organization: name: Spotify @@ -26,12 +33,70 @@ techdocs: sentry: organization: spotify +rollbar: + organization: spotify + accountToken: + $secret: + env: ROLLBAR_ACCOUNT_TOKEN + +newrelic: + api: + baseUrl: 'https://api.newrelic.com/v2' + key: NEW_RELIC_REST_API_KEY + +lighthouse: + baseUrl: http://localhost:3003 + +catalog: + rules: + - allow: [Component, API, Group, Template, Location] + processors: + githubApi: + privateToken: + $secret: + env: GITHUB_PRIVATE_TOKEN + bitbucketApi: + username: + $secret: + env: BITBUCKET_USERNAME + appPassword: + $secret: + env: BITBUCKET_APP_PASSWORD + gitlabApi: + privateToken: + $secret: + env: GITLAB_PRIVATE_TOKEN + azureApi: + privateToken: + $secret: + env: AZURE_PRIVATE_TOKEN + exampleEntityLocations: + github: + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml + - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml + auth: providers: google: development: +<<<<<<< HEAD appOrigin: 'http://localhost:3000/' secure: false +======= +>>>>>>> master clientId: $secret: env: AUTH_GOOGLE_CLIENT_ID @@ -40,8 +105,11 @@ auth: env: AUTH_GOOGLE_CLIENT_SECRET github: development: +<<<<<<< HEAD appOrigin: 'http://localhost:3000/' secure: false +======= +>>>>>>> master clientId: $secret: env: AUTH_GITHUB_CLIENT_ID @@ -53,8 +121,11 @@ auth: env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: +<<<<<<< HEAD appOrigin: 'http://localhost:3000/' secure: false +======= +>>>>>>> master clientId: $secret: env: AUTH_GITLAB_CLIENT_ID @@ -65,6 +136,7 @@ auth: $secret: env: GITLAB_BASE_URL saml: +<<<<<<< HEAD development: entryPoint: 'http://localhost:7001/' issuer: 'passport-saml' @@ -72,6 +144,12 @@ auth: development: appOrigin: 'http://localhost:3000/' secure: false +======= + entryPoint: "http://localhost:7001/" + issuer: "passport-saml" + okta: + development: +>>>>>>> master clientId: $secret: env: AUTH_OKTA_CLIENT_ID @@ -83,17 +161,42 @@ auth: env: AUTH_OKTA_AUDIENCE oauth2: development: +<<<<<<< HEAD appOrigin: 'http://localhost:3000/' secure: false +======= +>>>>>>> master clientId: $secret: env: AUTH_OAUTH2_CLIENT_ID clientSecret: $secret: env: AUTH_OAUTH2_CLIENT_SECRET - authorizationURL: + authorizationUrl: $secret: env: AUTH_OAUTH2_AUTH_URL - tokenURL: + tokenUrl: $secret: env: AUTH_OAUTH2_TOKEN_URL + auth0: + development: + clientId: + $secret: + env: AUTH_AUTH0_CLIENT_ID + clientSecret: + $secret: + env: AUTH_AUTH0_CLIENT_SECRET + domain: + $secret: + env: AUTH_AUTH0_DOMAIN + microsoft: + development: + clientId: + $secret: + env: AUTH_MICROSOFT_CLIENT_ID + clientSecret: + $secret: + env: AUTH_MICROSOFT_CLIENT_SECRET + tenantId: + $secret: + env: AUTH_MICROSOFT_TENANT_ID diff --git a/catalog-info.yaml b/catalog-info.yaml index 86b67f95cd..4221ee6dc6 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -7,6 +7,7 @@ metadata: annotations: github.com/project-slug: spotify/backstage backstage.io/github-actions-id: spotify/backstage + backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git spec: type: library owner: Spotify diff --git a/docs/FAQ.md b/docs/FAQ.md index 7d568a840e..4b5df3ace0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,6 +1,9 @@ -# FAQ +--- +id: FAQ +title: FAQ +--- -## Product FAQ: +## Product FAQ ### Can we call Backstage something different? So that it fits our company better? @@ -14,8 +17,7 @@ brand. No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into -Backstage by writing -[a plugin](/docs/FAQ.md#what-is-a-plugin-in-backstage). +Backstage by writing [a plugin](#what-is-a-plugin-in-backstage). ### How is Backstage licensed? @@ -36,12 +38,11 @@ more, read our blog post, Yes, we've already started releasing open source versions of some of the plugins we use here, and we'll continue to do so. -[Plugins](/docs/FAQ.md#what-is-a-plugin-in-backstage) are the -building blocks of functionality in Backstage. We have over 120 plugins inside -Spotify — many of those are specialized for our use, so will remain internal and -proprietary to us. But we estimate that about a third of our existing plugins -make good open source candidates. (And we'll probably end up writing some brand -new ones, too.) +[Plugins](#what-is-a-plugin-in-backstage) are the building blocks of +functionality in Backstage. We have over 120 plugins inside Spotify — many of +those are specialized for our use, so will remain internal and proprietary to +us. But we estimate that about a third of our existing plugins make good open +source candidates. (And we'll probably end up writing some brand new ones, too.) ### What's the roadmap for Backstage? @@ -66,7 +67,7 @@ valuable as you grow. Yes! The Backstage UI is built using Material-UI. With the theming capabilities of Material-UI, you are able to adapt the interface to your brand guidelines. -## Technical FAQ: +## Technical FAQ ### Why Material-UI? @@ -91,21 +92,31 @@ Node.js and GraphQL. ### What is the end-to-end user flow? The happy path story. There are three main user profiles for Backstage: the integrator, the -contributor, and the software engineer. +contributor, and the software engineer. -The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. +The **integrator** hosts the Backstage app and configures which plugins are +available to use in the app. The **contributor** adds functionality to the app by writing plugins. -The **software engineer** uses the app's functionality and interacts with its plugins. +The **software engineer** uses the app's functionality and interacts with its +plugins. ### What is a "plugin" in Backstage? -Plugins are what provide the feature functionality in Backstage. They are used to integrate different systems into Backstage's frontend, so that the developer gets a consistent UX, no matter what tool or service is being accessed on the other side. +Plugins are what provide the feature functionality in Backstage. They are used +to integrate different systems into Backstage's frontend, so that the developer +gets a consistent UX, no matter what tool or service is being accessed on the +other side. -Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data either from the backend or an API exposed through the proxy. +Each plugin is treated as a self-contained web app and can include almost any +type of content. Plugins all use a common set of platform APIs and reusable UI +components. Plugins can fetch data either from the backend or an API exposed +through the proxy. -Learn more about [the different components](https://github.com/spotify/backstage#overview) that make up Backstage. +Learn more about +[the different components](https://github.com/spotify/backstage#overview) that +make up Backstage. ### Do I have to write plugins in TypeScript? @@ -127,7 +138,15 @@ can browse and search for all available plugins. ### Which plugin is used the most at Spotify? -By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](/docs/FAQ.md#will-spotifys-internal-plugins-be-open-sourced-too)" above) +By far, our most-used plugin is our TechDocs plugin, which we use for creating +technical documentation. Our philosophy at Spotify is to treat "docs like code", +where you write documentation using the same workflow as you write your code. +This makes it easier to create, find, and update documentation. We hope to +release +[the open source version](https://github.com/spotify/backstage/issues/687) in +the future. (See also: +"[Will Spotify's internal plugins be open sourced, too?](#will-spotifys-internal-plugins-be-open-sourced-too)" +above) ### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? @@ -135,10 +154,10 @@ Contributors can add open source plugins to the plugins directory in [this monorepo](https://github.com/spotify/backstage). Integrators can then configure which open source plugins are available to use in their instance of the app. Open source plugins are downloaded as npm packages published in the -open source repository. While we encourage using the open source model, we -know there are cases where contributors might want to experiment internally or -keep their plugins closed source. Contributors writing closed source plugins -should develop them in the plugins directory in their own Backstage repository. +open source repository. While we encourage using the open source model, we know +there are cases where contributors might want to experiment internally or keep +their plugins closed source. Contributors writing closed source plugins should +develop them in the plugins directory in their own Backstage repository. Integrators also configure closed source plugins locally from the monorepo. ### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? @@ -149,7 +168,8 @@ stage. Hosting this project on GitHub does not exclude integrations with alternatives, such as [GitLab](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+GitLab) or Bitbucket. We believe that in time there will be plugins that will provide -functionality for these tools as well. Hopefully, contributed by the community! Also note, implementations of Backstage can be hosted wherever you feel suits +functionality for these tools as well. Hopefully, contributed by the community! +Also note, implementations of Backstage can be hosted wherever you feel suits your needs best. ### Who maintains Backstage? @@ -165,8 +185,8 @@ maintains Backstage in your own environment. ### Does Spotify provide a managed version of Backstage? -No, this is not a service offering. We build the piece of software, and -someone in your infrastructure team is responsible for +No, this is not a service offering. We build the piece of software, and someone +in your infrastructure team is responsible for [deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and maintaining it. @@ -186,16 +206,16 @@ Please report sensitive security issues via Spotify's No. Backstage does not collect any telemetry from any third party using the platform. Spotify, and the open source community, does have access to [GitHub Insights](https://github.com/features/insights), which contains -information such as contributors, commits, traffic, and dependencies. -Backstage is an open platform, but you are in control of your own data. You -control who has access to any data you provide to your version of Backstage and -who that data is shared with. +information such as contributors, commits, traffic, and dependencies. Backstage +is an open platform, but you are in control of your own data. You control who +has access to any data you provide to your version of Backstage and who that +data is shared with. ### Can Backstage be used to build something other than a developer portal? -Yes. The core frontend framework could be used for building any large-scale -web application where (1) multiple teams are building separate parts of the app, -and (2) you want the overall experience to be consistent. That being said, in +Yes. The core frontend framework could be used for building any large-scale web +application where (1) multiple teams are building separate parts of the app, and +(2) you want the overall experience to be consistent. That being said, in [Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project we will add features that are needed for developer portals and systems for managing software ecosystems. Our ambition will be to keep Backstage modular. diff --git a/docs/README.md b/docs/README.md index 2bda6a5aaa..c63a12b589 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,106 +1,3 @@ -# Documentation structure +# Documentation -**Note!** This documentation structure is very much work in progress. If (when, -really 😆) you find broken links or missing content, please create an issue or, -better yet, a pull request. - -- Overview - - [What is Backstage?](overview/what-is-backstage.md) - - [Backstage architecture](overview/architecture-overview.md) - - [Architecture and terminology](overview/architecture-terminology.md) - - [Roadmap](overview/roadmap.md) - - Getting started - - [Running Backstage locally](getting-started/index.md) - - [Installation](getting-started/installation.md) - - [Local development](getting-started/development-environment.md) - - [Demo deployment](https://backstage-demo.roadie.io) - - Production deployments - - [Create an App](getting-started/create-an-app.md) - - App configuration - - [Configuring App with plugins](getting-started/configure-app-with-plugins.md) - - [Customize the look-and-feel of your App](getting-started/app-custom-theme.md) - - Deployment scenarios - - [Kubernetes](getting-started/deployment-k8s.md) - - [Other](getting-started/deployment-other.md) - - Features - - Software Catalog - - [Overview](features/software-catalog/index.md) - - [System model](features/software-catalog/system-model.md) - - [YAML File Format](features/software-catalog/descriptor-format.md) - - [Populating the catalog](features/software-catalog/populating.md) - - [Extending the model](features/software-catalog/extending-the-model.md) - - [External integrations](features/software-catalog/external-integrations.md) - - [API](features/software-catalog/api.md) - - Software creation templates - - [Overview](features/software-templates/index.md) - - [Adding templates](features/software-templates/adding-templates.md) - - Extending the Scaffolder: - - [Overview](features/software-templates/extending/index.md) - - [Create your own Templater](features/software-templates/extending/create-your-own-templater.md) - - [Create your own Publisher](features/software-templates/extending/create-your-own-publisher.md) - - [Create your own Preparer](features/software-templates/extending/create-your-own-preparer.md) - - Docs-like-code - - [Overview](features/techdocs/README.md) - - [Getting Started](features/techdocs/getting-started.md) - - [Concepts](features/techdocs/concepts.md) - - [Reading Documentation](features/techdocs/reading-documentation.md) - - [Writing Documentation](features/techdocs/writing-documentation.md) - - [Publishing Documentation](features/techdocs/publishing-documentation.md) - - [Contributing](features/techdocs/contributing.md) - - [Debugging](features/techdocs/debugging.md) - - [FAQ](features/techdocs/FAQ.md) - - Plugins - - [Overview](plugins/index.md) - - [Existing plugins](plugins/existing-plugins.md) - - [Creating a new plugin](plugins/create-a-plugin.md) - - [Developing a plugin](plugins/plugin-development.md) - - [Structure of a plugin](plugins/structure-of-a-plugin.md) - - Backends and APIs - - [Proxying](plugins/proxying.md) - - [Backstage backend plugin](plugins/backend-plugin.md) - - [Call existing API](plugins/call-existing-api.md) - - Testing - - [Overview](plugins/testing.md) - - Publishing - - [Open source and NPM](plugins/publishing.md) - - [Private/internal (non-open source)](plugins/publish-private.md) - - Configuration - - [Overview](conf/index.md) - - [Reading Configuration](conf/reading.md) - - [Writing Configuration](conf/writing.md) - - [Defining Configuration](conf/defining.md) - - Authentication and identity - - [Overview](auth/index.md) - - [Add auth provider](auth/add-auth-provider.md) - - [Auth backend](auth/auth-backend.md) - - [OAuth](auth/oauth.md) - - [Glossary](auth/glossary.md) - - Designing for Backstage - - [Backstage Design Language System (DLS)](dls/design.md) - - [Storybook -- reusable UI components](http://storybook.backstage.io) - - [Contributing to Storybook](dls/contributing-to-storybook.md) - - [Figma resources](dls/figma.md) - - API references - - TypeScript API - - [Utility APIs](api/utility-apis.md) - - [Utility API References](reference/utility-apis/README.md) - - [createPlugin](reference/createPlugin.md) - - [createPlugin-feature-flags](reference/createPlugin-feature-flags.md) - - [createPlugin-router](reference/createPlugin-router.md) - - Backend APIs - - [Backend](api/backend.md) - - Tutorials - - [Overview](tutorials/index.md) - - Architecture Decision Records (ADRs) - - [Overview](architecture-decisions/index.md) - - [ADR001 - Architecture Decision Record (ADR) log](architecture-decisions/adr001-add-adr-log.md) - - [ADR002 - Default Software Catalog File Format](architecture-decisions/adr002-default-catalog-file-format.md) - - [ADR003 - Avoid Default Exports and Prefer Named Exports](architecture-decisions/adr003-avoid-default-exports.md) - - [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md) - - [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md) - - [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md) - - [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md) - - [ADR008 - Default Catalog File Name](architecture-decisions/adr008-default-catalog-file-name.md) - - [Contribute](../CONTRIBUTING.md) - - [Support](overview/support.md) - - [FAQ](FAQ.md) +The Backstage documentation is available at https://backstage.io/docs diff --git a/docs/api/backend.md b/docs/api/backend.md index e69de29bb2..bc44b8350c 100644 --- a/docs/api/backend.md +++ b/docs/api/backend.md @@ -0,0 +1,6 @@ +--- +id: backend +title: Backend +--- + +## TODO diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index f87db15e7e..b4c2fd161e 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -1,4 +1,7 @@ -# Utility APIs +--- +id: utility-apis +title: Utility APIs +--- ## Introduction @@ -68,18 +71,23 @@ import { AlertApiForwarder, ErrorApiForwarder, ErrorAlerter, + ConfigApi } from '@backstage/core'; -const builder = ApiRegistry.builder(); -// The alert API is a self-contained implementation that shows alerts to the user. -const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); +const apis = (config: ConfigApi) => { + const builder = ApiRegistry.builder(); -// The error API uses the alert API to send error notifications to the user. -builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); + // The alert API is a self-contained implementation that shows alerts to the user. + const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); + + // The error API uses the alert API to send error notifications to the user. + builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); + return builder.build(); +} const app = createApp({ - apis: apiBuilder.build(), + apis, // ... other config }); ``` @@ -153,7 +161,7 @@ The figure below shows the relationship between fooApiRef.
-Figure showing the relationship between utility APIs, the apps that provide them, and the plugins that consume them +Figure showing the relationship between utility APIs, the apps that provide them, and the plugins that consume them
The current method for connecting Utility API providers and consumers is via the diff --git a/docs/architecture-decisions/adr001-add-adr-log.md b/docs/architecture-decisions/adr001-add-adr-log.md index e6ab39aea4..8a484fa264 100644 --- a/docs/architecture-decisions/adr001-add-adr-log.md +++ b/docs/architecture-decisions/adr001-add-adr-log.md @@ -1,4 +1,8 @@ -# ADR001: Architecture Decision Record (ADR) log +--- +id: adrs-adr001 +title: ADR001: Architecture Decision Record (ADR) log +sidebar_label: ADR001 +--- | Created | Status | | ---------- | ------ | diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index 00c8b0947d..c1cb719bf5 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -1,4 +1,8 @@ -# ADR002: Default Software Catalog File Format +--- +id: adrs-adr002 +title: ADR002: Default Software Catalog File Format +sidebar_label: ADR002 +--- | Created | Status | | ---------- | ------ | diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md index 7becebaa4b..7a8df28f49 100644 --- a/docs/architecture-decisions/adr003-avoid-default-exports.md +++ b/docs/architecture-decisions/adr003-avoid-default-exports.md @@ -1,4 +1,8 @@ -# ADR003: Avoid Default Exports and Prefer Named Exports +--- +id: adrs-adr003 +title: ADR003: Avoid Default Exports and Prefer Named Exports +sidebar_label: ADR003 +--- | Created | Status | | ---------- | ------ | diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index 54132773c9..077f8c35d1 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -1,4 +1,8 @@ -# ADR004: Module Export Structure +--- +id: adrs-adr004 +title: ADR004: Module Export Structure +sidebar_label: ADR004 +--- | Created | Status | | ---------- | ------ | diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index d607dc62fb..8f39acd1cf 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -1,4 +1,8 @@ -# ADR005: Catalog Core Entities +--- +id: adrs-adr005 +title: ADR005: Catalog Core Entities +sidebar_label: ADR005 +--- | Created | Status | | ---------- | ------ | @@ -18,7 +22,7 @@ Backstage should eventually support the following core entities: - **Resources** are physical or virtual infrastructure needed to operate a component -![Catalog Core Entities](catalog-core-entities.png) +![Catalog Core Entities](../assets/architecture-decisions/catalog-core-entities.png) For now, we'll start by only implementing support for the Component entity in the Backstage catalog. This can later be extended to APIs, Resources and other diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 21e6d5b637..e48fceb562 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -1,4 +1,8 @@ -# ADR006: Avoid React.FC and React.SFC +--- +id: adrs-adr006 +title: ADR006: Avoid React.FC and React.SFC +sidebar_label: ADR006 +--- ## Context diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md index bdb221cea3..773f08be30 100644 --- a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md +++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md @@ -1,4 +1,8 @@ -# ADR007: Use MSW to mock http requests +--- +id: adrs-adr007 +title: ADR007: Use MSW to mock http requests +sidebar_label: ADR007 +--- ## Context diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md index 23910f634f..979ea4de33 100644 --- a/docs/architecture-decisions/adr008-default-catalog-file-name.md +++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md @@ -1,4 +1,8 @@ -# ADR008: Default Catalog File Name +--- +id: adrs-adr008 +title: ADR008: Default Catalog File Name +sidebar_label: ADR008 +--- ## Background diff --git a/docs/architecture-decisions/index.md b/docs/architecture-decisions/index.md index 7762a9c827..91d2c668d7 100644 --- a/docs/architecture-decisions/index.md +++ b/docs/architecture-decisions/index.md @@ -1,4 +1,8 @@ -# Architecture Decision Records (ADR) +--- +id: adrs-overview +title: Architecture Decision Records (ADR) +sidebar_label: Overview +--- The substantial architecture decisions made in the Backstage project lives here. For more information about ADRs, when to write them, and why, please see @@ -19,7 +23,10 @@ Records should be stored under the `architecture-decisions` directory. - Submit a pull request - Address and integrate feedback from the community - Eventually, assign a number -- Add the full path of the ADR to the [`mkdocs.yml`](/mkdocs.yml) +- Add the path of the ADR to the microsite sidebar in + [`sidebars.json`](https://github.com/spotify/backstage/blob/master/microsite/sidebars.json) +- Add the path of the ADR to the + [`mkdocs.yml`](https://github.com/spotify/backstage/blob/master/mkdocs.yml) - Merge the pull request ## Superseding an ADR diff --git a/docs/architecture-decisions/catalog-core-entities.png b/docs/assets/architecture-decisions/catalog-core-entities.png similarity index 100% rename from docs/architecture-decisions/catalog-core-entities.png rename to docs/assets/architecture-decisions/catalog-core-entities.png diff --git a/docs/overview/architecture-overview/backstage-typical-architecture.png b/docs/assets/architecture-overview/backstage-typical-architecture.png similarity index 100% rename from docs/overview/architecture-overview/backstage-typical-architecture.png rename to docs/assets/architecture-overview/backstage-typical-architecture.png diff --git a/docs/overview/architecture-overview/circle-ci-plugin-architecture.png b/docs/assets/architecture-overview/circle-ci-plugin-architecture.png similarity index 100% rename from docs/overview/architecture-overview/circle-ci-plugin-architecture.png rename to docs/assets/architecture-overview/circle-ci-plugin-architecture.png diff --git a/docs/overview/architecture-overview/circle-ci.png b/docs/assets/architecture-overview/circle-ci.png similarity index 100% rename from docs/overview/architecture-overview/circle-ci.png rename to docs/assets/architecture-overview/circle-ci.png diff --git a/docs/overview/architecture-overview/containerised.png b/docs/assets/architecture-overview/containerised.png similarity index 100% rename from docs/overview/architecture-overview/containerised.png rename to docs/assets/architecture-overview/containerised.png diff --git a/docs/overview/architecture-overview/core-vs-plugin-components-highlighted.png b/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png similarity index 100% rename from docs/overview/architecture-overview/core-vs-plugin-components-highlighted.png rename to docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png diff --git a/docs/overview/architecture-overview/lighthouse-plugin-architecture.png b/docs/assets/architecture-overview/lighthouse-plugin-architecture.png similarity index 100% rename from docs/overview/architecture-overview/lighthouse-plugin-architecture.png rename to docs/assets/architecture-overview/lighthouse-plugin-architecture.png diff --git a/docs/overview/architecture-overview/lighthouse-plugin.png b/docs/assets/architecture-overview/lighthouse-plugin.png similarity index 100% rename from docs/overview/architecture-overview/lighthouse-plugin.png rename to docs/assets/architecture-overview/lighthouse-plugin.png diff --git a/docs/overview/architecture-overview/tech-radar-plugin-architecture.png b/docs/assets/architecture-overview/tech-radar-plugin-architecture.png similarity index 100% rename from docs/overview/architecture-overview/tech-radar-plugin-architecture.png rename to docs/assets/architecture-overview/tech-radar-plugin-architecture.png diff --git a/docs/overview/architecture-overview/tech-radar-plugin.png b/docs/assets/architecture-overview/tech-radar-plugin.png similarity index 100% rename from docs/overview/architecture-overview/tech-radar-plugin.png rename to docs/assets/architecture-overview/tech-radar-plugin.png diff --git a/docs/contributorheader.png b/docs/assets/contributorheader.png similarity index 100% rename from docs/contributorheader.png rename to docs/assets/contributorheader.png diff --git a/docs/dls/DLS.png b/docs/assets/dls/DLS.png similarity index 100% rename from docs/dls/DLS.png rename to docs/assets/dls/DLS.png diff --git a/docs/assets/dls/designheader-updated.png b/docs/assets/dls/designheader-updated.png new file mode 100644 index 0000000000..56c5a56abb Binary files /dev/null and b/docs/assets/dls/designheader-updated.png differ diff --git a/docs/dls/designheader.png b/docs/assets/dls/designheader.png similarity index 100% rename from docs/dls/designheader.png rename to docs/assets/dls/designheader.png diff --git a/docs/dls/running-storybook.png b/docs/assets/dls/running-storybook.png similarity index 100% rename from docs/dls/running-storybook.png rename to docs/assets/dls/running-storybook.png diff --git a/docs/dls/storybook-page.png b/docs/assets/dls/storybook-page.png similarity index 100% rename from docs/dls/storybook-page.png rename to docs/assets/dls/storybook-page.png diff --git a/docs/getting-started/create-app_output.png b/docs/assets/getting-started/create-app_output.png similarity index 100% rename from docs/getting-started/create-app_output.png rename to docs/assets/getting-started/create-app_output.png diff --git a/docs/getting-started/create-plugin_output.png b/docs/assets/getting-started/create-plugin_output.png similarity index 100% rename from docs/getting-started/create-plugin_output.png rename to docs/assets/getting-started/create-plugin_output.png diff --git a/docs/headline.png b/docs/assets/headline.png similarity index 100% rename from docs/headline.png rename to docs/assets/headline.png diff --git a/docs/plugins/my-plugin_screenshot.png b/docs/assets/my-plugin_screenshot.png similarity index 100% rename from docs/plugins/my-plugin_screenshot.png rename to docs/assets/my-plugin_screenshot.png diff --git a/docs/assets/pop.png b/docs/assets/pop.png new file mode 100644 index 0000000000..441126f33c Binary files /dev/null and b/docs/assets/pop.png differ diff --git a/docs/assets/software-catalog/bsc-edit.png b/docs/assets/software-catalog/bsc-edit.png new file mode 100644 index 0000000000..d1ac1f52e5 Binary files /dev/null and b/docs/assets/software-catalog/bsc-edit.png differ diff --git a/docs/assets/software-catalog/bsc-extend.png b/docs/assets/software-catalog/bsc-extend.png new file mode 100644 index 0000000000..54d660ea70 Binary files /dev/null and b/docs/assets/software-catalog/bsc-extend.png differ diff --git a/docs/assets/software-catalog/bsc-register-1.png b/docs/assets/software-catalog/bsc-register-1.png new file mode 100644 index 0000000000..117b2ea8ea Binary files /dev/null and b/docs/assets/software-catalog/bsc-register-1.png differ diff --git a/docs/assets/software-catalog/bsc-register-2.png b/docs/assets/software-catalog/bsc-register-2.png new file mode 100644 index 0000000000..fd1ea7b998 Binary files /dev/null and b/docs/assets/software-catalog/bsc-register-2.png differ diff --git a/docs/assets/software-catalog/bsc-search.png b/docs/assets/software-catalog/bsc-search.png new file mode 100644 index 0000000000..8e417cb076 Binary files /dev/null and b/docs/assets/software-catalog/bsc-search.png differ diff --git a/docs/assets/software-catalog/bsc-starred.png b/docs/assets/software-catalog/bsc-starred.png new file mode 100644 index 0000000000..27db19c842 Binary files /dev/null and b/docs/assets/software-catalog/bsc-starred.png differ diff --git a/docs/features/software-catalog/service-catalog-home.png b/docs/assets/software-catalog/service-catalog-home.png similarity index 100% rename from docs/features/software-catalog/service-catalog-home.png rename to docs/assets/software-catalog/service-catalog-home.png diff --git a/docs/assets/software-catalog/software-model-core-entities.png b/docs/assets/software-catalog/software-model-core-entities.png new file mode 100644 index 0000000000..b718b7527c Binary files /dev/null and b/docs/assets/software-catalog/software-model-core-entities.png differ diff --git a/docs/features/software-templates/assets/added-to-the-catalog-list.png b/docs/assets/software-templates/added-to-the-catalog-list.png similarity index 100% rename from docs/features/software-templates/assets/added-to-the-catalog-list.png rename to docs/assets/software-templates/added-to-the-catalog-list.png diff --git a/docs/features/software-templates/assets/complete.png b/docs/assets/software-templates/complete.png similarity index 100% rename from docs/features/software-templates/assets/complete.png rename to docs/assets/software-templates/complete.png diff --git a/docs/features/software-templates/assets/create.png b/docs/assets/software-templates/create.png similarity index 100% rename from docs/features/software-templates/assets/create.png rename to docs/assets/software-templates/create.png diff --git a/docs/features/software-templates/assets/failed.png b/docs/assets/software-templates/failed.png similarity index 100% rename from docs/features/software-templates/assets/failed.png rename to docs/assets/software-templates/failed.png diff --git a/docs/features/software-templates/assets/go-to-catalog.png b/docs/assets/software-templates/go-to-catalog.png similarity index 100% rename from docs/features/software-templates/assets/go-to-catalog.png rename to docs/assets/software-templates/go-to-catalog.png diff --git a/docs/features/software-templates/assets/running.png b/docs/assets/software-templates/running.png similarity index 100% rename from docs/features/software-templates/assets/running.png rename to docs/assets/software-templates/running.png diff --git a/docs/features/software-templates/assets/template-picked-2.png b/docs/assets/software-templates/template-picked-2.png similarity index 100% rename from docs/features/software-templates/assets/template-picked-2.png rename to docs/assets/software-templates/template-picked-2.png diff --git a/docs/features/software-templates/assets/template-picked.png b/docs/assets/software-templates/template-picked.png similarity index 100% rename from docs/features/software-templates/assets/template-picked.png rename to docs/assets/software-templates/template-picked.png diff --git a/docs/assets/techdocs/documentation-template.png b/docs/assets/techdocs/documentation-template.png new file mode 100644 index 0000000000..1f44ad27c6 Binary files /dev/null and b/docs/assets/techdocs/documentation-template.png differ diff --git a/docs/assets/techdocs/techdocs_big_picture.png b/docs/assets/techdocs/techdocs_big_picture.png new file mode 100644 index 0000000000..ffe437180f Binary files /dev/null and b/docs/assets/techdocs/techdocs_big_picture.png differ diff --git a/docs/api/utility-apis-fig1.svg b/docs/assets/utility-apis-fig1.svg similarity index 100% rename from docs/api/utility-apis-fig1.svg rename to docs/assets/utility-apis-fig1.svg diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 33f1d335a0..7e4f5c8e2f 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -1,4 +1,7 @@ -# Adding authentication providers +--- +id: add-auth-provider +title: Adding authentication providers +--- ## Passport @@ -45,22 +48,35 @@ provider class which implements a handler for the chosen framework. #### Adding an OAuth based provider If we're adding an `OAuth` based provider we would implement the -[OAuthProviderHandlers](#OAuthProviderHandlers) interface. +[OAuthProviderHandlers](#OAuthProviderHandlers) interface. By implementing this +interface we can use the `OAuthProvider` class provided by `lib/oauth`, meaning +we don't need to implement the full +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) interface that providers +otherwise need to implement. -The provider class takes the provider's configuration as a class parameter. It -also imports the `Strategy` from the passport package. +The provider class takes the provider's options as a class parameter. It also +imports the `Strategy` from the passport package. ```ts import { Strategy as ProviderAStrategy } from 'passport-provider-a'; +export type ProviderAProviderOptions = OAuthProviderOptions & { + // extra options here +} + export class ProviderAAuthProvider implements OAuthProviderHandlers { - private readonly providerConfig: AuthProviderConfig; private readonly _strategy: ProviderAStrategy; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; + constructor(options: ProviderAProviderOptions) { this._strategy = new ProviderAStrategy( - { ...providerConfig.options }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + passReqToCallback: false as true, + response_type: 'code', + /// ... etc + } verifyFunction, // See the "Verify Callback" section ); } @@ -79,14 +95,18 @@ An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. ```ts +type ProviderAOptions = { + // ... +}; + export class ProviderAAuthProvider implements AuthProviderRouteHandlers { - private readonly providerConfig: AuthProviderConfig; private readonly _strategy: ProviderAStrategy; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; + constructor(options: ProviderAOptions) { this._strategy = new ProviderAStrategy( - { ...providerConfig.options }, + { + // ... + }, verifyFunction, // See the "Verify Callback" section ); } @@ -98,31 +118,61 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { } ``` -#### Create method +#### Factory function -Each provider exports a create method that creates the provider instance, -optionally extending a supported authorization framework. This method exists to -allow for flexibility if additional frameworks are supported in the future. +Each provider exports a factory function that instantiates the provider. The +factory should implement [AuthProviderFactory](#AuthProviderFactory), which +passes in a object with utilities for configuration, logging, token issuing, +etc. The factory should return an implementation of +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers). -Implementing OAuth by returning an instance of `OAuthProvider` based of the -provider's class: +The factory is what decides the mapping from +[static configuration](../conf/index.md) to the creation of auth providers. For +example, OAuth providers use `OAuthEnvironmentHandler` to allow for multiple +different configurations, one for each environment, which looks like this; ```ts -export function createProviderAProvider(config: AuthProviderConfig) { - const provider = new ProviderAAuthProvider(config); - const oauthProvider = new OAuthProvider(provider, config.provider, true); - return oauthProvider; -} +export const createOktaProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + // read options from config + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + + // instantiate our OAuthProviderHandlers implementation + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); + + // Wrap the OAuthProviderHandlers with OAuthProvider, which implements AuthProviderRouteHandlers + return OAuthProvider.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + }); ``` -Not extending with OAuth, the main difference here is that the create method is -returning a instance of the class without adding the OAuth authorization -framework to it. +The purpose of the different environments is to allow for a single auth-backend +to serve as the authentication service for multiple different frontend +environments, such as local development, staging, and production. + +The factory function for other providers can be a lot simpler, as they might not +have configuration for each environment. Looking something like this: ```ts -export function createProviderAProvider(config: AuthProviderConfig) { - return new ProviderAAuthProvider(config); -} +export const createProviderAProvider: AuthProviderFactory = ({ config }) => { + const a = config.getString('a'); + const b = config.getString('b'); + + return new ProviderAAuthProvider({ a, b }); +}; ``` #### Verify Callback @@ -141,7 +191,7 @@ export function createProviderAProvider(config: AuthProviderConfig) { > http://www.passportjs.org/docs/configure/ **`plugins/auth-backend/src/providers/providerA/index.ts`** is simply -re-exporting the create method to be used for hooking the provider up to the +re-exporting the factory function to be used for hooking the provider up to the backend. ```ts @@ -150,26 +200,14 @@ export { createProviderAProvider } from './provider'; ### Hook it up to the backend -**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be -configured properly so you need to add it to the list of configured providers, -all of which implement [AuthProviderConfig](#AuthProviderConfig): - -```ts -export const providers = [ - { - provider: 'providerA', # used as an identifier - options: { ... }, # consult the provider documentation for which options you should provide - disableRefresh: true # if the provider lacks refresh tokens - }, -``` - **`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling -`createAuthProviderRouter` on each provider. You need to import the create -method from the provider and add it to the factory: +`createAuthProviderRouter` on each provider. You need to import the factory +function from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA'; + const factories: { [providerId: string]: AuthProviderFactory } = { providerA: createProviderAProvider, }; @@ -200,10 +238,21 @@ web browser and you should be able to trigger the authorization flow. ```ts export interface OAuthProviderHandlers { - start(req: express.Request, options: any): Promise; - handler(req: express.Request): Promise; - refresh?(refreshToken: string, scope: string): Promise; - logout?(): Promise; + start( + req: express.Request, + options: Record, + ): Promise; + handler( + req: express.Request, + ): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; + refresh?( + refreshToken: string, + scope: string, + ): Promise>; + logout?(): Promise; } ``` @@ -218,12 +267,17 @@ export interface AuthProviderRouteHandlers { } ``` -##### AuthProviderConfig +##### AuthProviderFactory ```ts -export type AuthProviderConfig = { - provider: string; - options: any; - disableRefresh?: boolean; +export type AuthProviderFactoryOptions = { + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; }; + +export type AuthProviderFactory = ( + options: AuthProviderFactoryOptions, +) => AuthProviderRouteHandlers; ``` diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md new file mode 100644 index 0000000000..0147c66dd9 --- /dev/null +++ b/docs/auth/auth-backend-classes.md @@ -0,0 +1,132 @@ +--- +id: auth-backend-classes +title: Auth backend classes +--- + +## How Does Authentication Work? + +The Backstage application can use various authentication providers for +authentication. A provider has to implement an `AuthProviderRouteHandlers` +interface for handling authentication. This interface consists of four methods. +Each of these methods is hosted at an endpoint `/auth/[provider]/method`, where +`method` performs a certain operation as follows: + +``` + /auth/[provider]/start -> start + /auth/[provider]/handler/frame -> frameHandler + /auth/[provider]/refresh -> refresh + /auth/[provider]/logout -> logout +``` + +For more information on how these methods are used and for which purpose, refer +to the documentation [here](oauth.md). + +For details on the parameters, input and output conditions for each method, +refer to the type documentation under +`plugins/auth-backend/src/providers/types.ts`. + +There are currently two different classes for two authentication mechanisms that +implement this interface: an `OAuthAdapter` for [OAuth](https://oauth.net/2/) +based mechanisms and a `SAMLAuthProvider` for +[SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) +based mechanisms. + +### OAuth mechanisms + +Currently OAuth is assumed to be the de facto authentication mechanism for +Backstage based applications. + +Backstage comes with a "batteries-included" set of supported commonly used OAuth +providers: Okta, Github, Google, Gitlab, and a generic OAuth2 provider. + +All of these use the authorization flow of OAuth2 to implement authentication. + +If your authentication provider is any of the above mentioned (except generic +OAuth2) providers, you can configure them by setting the right variables in +`app-config.yaml` under the `auth` section. + +### Configuration + +Each authentication provider (except SAML) needs five parameters: an OAuth +client ID, a client secret, an authorization endpoint and a token endpoint, and +an app origin. The app origin is the URL at which the frontend of the +application is hosted, and it is read from the `app.baseUrl` config. This is +required because the application opens a popup window to perform the +authentication, and once the flow is completed, the popup window sends a +`postMessage` to the frontend application to indicate the result of the +operation. Also this URL is used to verify that authentication requests are +coming from only this endpoint. + +These values are configured via the `app-config.yaml` present in the root of +your app folder. + +``` +auth: + providers: + google: + development: + clientId: + $secret: + env: AUTH_GOOGLE_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GOOGLE_CLIENT_SECRET + github: + development: + clientId: + $secret: + env: AUTH_GITHUB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITHUB_CLIENT_SECRET + enterpriseInstanceUrl: + $secret: + env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + gitlab: + development: + clientId: + $secret: + ... +``` + +## Technical Notes + +### OAuthEnvironmentHandler + +The concept of an "env" is core to the way the auth backend works. It uses an +`env` query parameter to identify the environment in which the application is +running (`development`, `staging`, `production`, etc). Each runtime can support +multiple environments at the same time and the right handler for each request is +identified and dispatched to based on the `env` parameter. All +`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`. + +To instantiate multiple OAuth providers for different environments, use +`OAuthEnvironmentHandler.mapConfig`. It's a helper to iterate over a +configuration object that is a map of environment to configurations. See one of +the existing OAuth providers for an example of how it is used. + +Given the following configuration: + +```yaml +development: + clientId: abc + clientSecret: secret +production: + clientId: xyz + clientSecret: supersecret +``` + +The `OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)` call will +split the `config` by the top level `development` and `production` keys, and +pass on each block as `envConfig`. + +For a list of currently available providers, look in the `factories` module +located in `plugins/auth-backend/src/providers/factories.ts` + +### OAuth2 provider + +The `oauth2` provider abstracts a generic **OAuth2 + OIDC** based authentication +provider. What this means is that after the application has been given +permission by the user, the `authorization code` will be exchanged for an +`access_token`, a `refresh_token` and an `id_token`. This `id_token` is used to +obtain an email id of the user, which is then used for creating the session. diff --git a/docs/auth/auth-backend.md b/docs/auth/auth-backend.md index e69de29bb2..579d2e2cda 100644 --- a/docs/auth/auth-backend.md +++ b/docs/auth/auth-backend.md @@ -0,0 +1,6 @@ +--- +id: auth-backend +title: Auth backend +--- + +## TODO diff --git a/docs/auth/glossary.md b/docs/auth/glossary.md index 6408587273..607326b3a5 100644 --- a/docs/auth/glossary.md +++ b/docs/auth/glossary.md @@ -1,4 +1,7 @@ -# Glossary +--- +id: glossary +title: Glossary +--- - **Popup** - A separate browser window opened on top of the previous one. - **OAuth** - More specifically OAuth 2.0, a standard protocol for diff --git a/docs/auth/index.md b/docs/auth/index.md index 6f9af17cba..b885db0820 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -1,4 +1,7 @@ -# User Authentication and Authorization in Backstage +--- +id: index +title: User Authentication and Authorization in Backstage +--- ## Summary diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md index d348dd2f93..b7013a310d 100644 --- a/docs/auth/oauth.md +++ b/docs/auth/oauth.md @@ -1,4 +1,7 @@ -# OAuth and OpenID Connect +--- +id: oauth +title: OAuth and OpenID Connect +--- This section describes how Backstage allows plugins to request OAuth Access Tokens and OpenID Connect ID Tokens on behalf of the user, to be used for auth @@ -101,6 +104,8 @@ request an access token. The following diagram visualizes the flow described in the previous section. +![](oauth-popup-flow.svg) + - -![](oauth-popup-flow.svg) diff --git a/docs/conf/defining.md b/docs/conf/defining.md index 0b15108f43..b28d6b8362 100644 --- a/docs/conf/defining.md +++ b/docs/conf/defining.md @@ -1,4 +1,7 @@ -# Defining Configuration for your Plugin +--- +id: defining +title: Defining Configuration for your Plugin +--- There is currently no tooling support or helpers for defining plugin configuration. But it's on the roadmap. diff --git a/docs/conf/index.md b/docs/conf/index.md index af1635d627..bc1d7e8e1f 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -1,4 +1,7 @@ -# Static Configuration in Backstage +--- +id: index +title: Static Configuration in Backstage +--- ## Summary diff --git a/docs/conf/reading.md b/docs/conf/reading.md index 9979f0360e..76d1f7f6ea 100644 --- a/docs/conf/reading.md +++ b/docs/conf/reading.md @@ -1,4 +1,7 @@ -# Reading Backstage Configuration +--- +id: reading +title: Reading Backstage Configuration +--- ## Config API @@ -98,7 +101,7 @@ A good pattern for reading optional configuration values is to use the `??` operator. For example: ```ts -const title = config.getString('my-plugin.title') ?? 'My Plugin'; +const title = config.getOptionalString('my-plugin.title') ?? 'My Plugin'; ``` To read required configuration, simply use the methods without `Optional`, for diff --git a/docs/conf/writing.md b/docs/conf/writing.md index 8ab397846d..da2a10a86d 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -1,4 +1,7 @@ -# Writing Backstage Configuration Files +--- +id: writing +title: Writing Backstage Configuration Files +--- ## File Format diff --git a/docs/dls/contributing-to-storybook.md b/docs/dls/contributing-to-storybook.md index 7dbe72f6d3..f0c2201a98 100644 --- a/docs/dls/contributing-to-storybook.md +++ b/docs/dls/contributing-to-storybook.md @@ -1,4 +1,10 @@ -# Contributing to Storybook +--- +id: contributing-to-storybook +title: Contributing to Storybook +--- + +You find our storybook at +[http://backstage.io/storybook](http://backstage.io/storybook) ## Creating a new Story @@ -26,7 +32,7 @@ core Go to `packages/storybook`, run `yarn install` and install the dependencies, then run the following on your command line: `yarn start` -![](running-storybook.png) +![](../assets/dls/running-storybook.png) _You should see a log like the image above._ @@ -34,4 +40,4 @@ If everything worked out, your server will be running on **port 6006**, go to your browser and navigate to `http://localhost:6006/`. You should be able to navigate and see the Storybook page. -![](storybook-page.png) +![](../assets/dls/storybook-page.png) diff --git a/docs/dls/design.md b/docs/dls/design.md index e3816bd2e5..23ba84024a 100644 --- a/docs/dls/design.md +++ b/docs/dls/design.md @@ -1,4 +1,9 @@ -![header](designheader.png) +--- +id: design +title: Design +--- + +![header](../assets/dls/designheader-updated.png) Much like Backstage Open Source, this is a _living_ document! We'll keep this updated as we evolve our practices! @@ -60,7 +65,7 @@ that is shaped by user experience and user interface decisions made by our Backstage Design Team. Also note, we encourage you to take the core experience we’ve crafted and add custom theming to better represent your organization! -![dls](DLS.png) +![dls](../assets/dls/DLS.png) ## ✅ Our Priorities @@ -106,18 +111,17 @@ picked up by our team as something to be added to our design system. ## ✏️ Resources -**[Storybook](http://storybook.backstage.io/)** - where you can view our +**[Storybook](http://backstage.io/storybook)** - where you can view our components. If you’d like to help build up our design system, you can also add components we’ve designed to the Storybook as well. +**[Figma](https://www.figma.com/@backstage)** - we're stoked to be using Figma +Community to share our design assets. You can duplicate our UI Kit +and design your own plugin for Backstage. + **[Discord](https://discord.gg/EBHEGzX)** - all design questions should be directed to the _#design_ channel. -**Documentation** - -- Patterns (stay tuned) -- Figma files/libraries (stay tuned) - ## 🔮 Future ### Contributions from designers diff --git a/docs/dls/figma.md b/docs/dls/figma.md index 8fe6ea8d0f..14de2f5980 100644 --- a/docs/dls/figma.md +++ b/docs/dls/figma.md @@ -1 +1,7 @@ -We have a [Figma component library](https://www.figma.com/@backstage) that you can use to build your own plugins for Backstage. +--- +id: figma +title: Figma +--- + +We have a [Figma component library](https://www.figma.com/@backstage) that you +can use to build your own plugins for Backstage. diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index e69de29bb2..332c1e505b 100644 --- a/docs/features/software-catalog/api.md +++ b/docs/features/software-catalog/api.md @@ -0,0 +1,6 @@ +--- +id: software-catalog-api +title: API +--- + +## TODO diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md new file mode 100644 index 0000000000..be6314e494 --- /dev/null +++ b/docs/features/software-catalog/configuration.md @@ -0,0 +1,58 @@ +--- +id: software-catalog-configuration +title: Catalog Configuration +--- + +## Static Location Configuration + +To enable declarative catalog setups, it is possible to add locations to the +catalog via [static configuration](../../conf/index.md). Locations are added to +the catalog under the `catalog.locations` key, for example: + +```yaml +catalog: + locations: + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml +``` + +The locations added through static configuration can not be removed through the +catalog locations API. To remove the locations, you have to remove them from the +configuration. + +## Catalog Rules + +By default the catalog will only allow ingestion of entities with the kind +`Component`, `API` and `Location`. In order to allow entities of other kinds to +be added, you need to add rules to the catalog. Rules are added either in a +separate `catalog.rules` key, or added to statically configured locations. + +For example, given the following configuration: + +```yaml +catalog: + rules: + - allow: [Component, API, Location, Template] + + locations: + - type: github + target: https://github.com/org/example/blob/master/org-data.yaml + rules: + - allow: [Group] +``` + +We are able to add entities of kind `Component`, `API`, `Location`, or +`Template` from any location, and `Group` entities from the `org-data.yaml`, +which will also be read as statically configured location. + +Note that if the `catalog.rules` key is present it will replace the default +value, meaning that you need to add rules for the default kinds if you want +those to still be allowed. + +The following configuration will reject any kind of entities from being added to +the catalog: + +```yaml +catalog: + rules: [] +``` diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 68cee2352c..78dfcf365c 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1,4 +1,8 @@ -# Descriptor Format of Catalog Entities +--- +id: descriptor-format +title: Descriptor Format of Catalog Entities +sidebar_label: YAML File Format +--- This section describes the default data shape and semantics of catalog entities. @@ -14,6 +18,8 @@ humans. However, the structure and semantics is the same in both cases. - [Common to All Kinds: The Envelope](#common-to-all-kinds-the-envelope) - [Common to All Kinds: The Metadata](#common-to-all-kinds-the-metadata) - [Kind: Component](#kind-component) +- [Kind: Template](#kind-template) +- [Kind: API](#kind-api) ## Overall Shape Of An Entity @@ -36,6 +42,7 @@ software catalog API. "labels": { "system": "public-websites" }, + "tags": ["java"], "name": "artist-web", "uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2" }, @@ -60,6 +67,8 @@ metadata: annotations: example.com/service-discovery: artistweb circleci.com/project-slug: gh/example-org/artist-website + tags: + - java spec: type: website lifecycle: production @@ -80,7 +89,7 @@ The root envelope object has the following structure. ### `apiVersion` and `kind` [required] The `kind` is the high level entity type being described. -[ADR005](/docs/architecture-decisions/adr005-catalog-core-entities.md) describes +[ADR005](../../architecture-decisions/adr005-catalog-core-entities.md) describes a number of core kinds that plugins can know of and understand, but an organization using Backstage is free to also add entities of other kinds to the catalog. @@ -226,6 +235,20 @@ The `backstage.io/` prefix is reserved for use by Backstage core components. Values can be of any length, but are limited to being strings. +### `tags` [optional] + +A list of single-valued strings, for example to classify catalog entities in +various ways. This is different to the labels in metadata, as labels are +key-value pairs. + +The values are user defined, for example the programming language used for the +component, like `java` or `go`. + +This field is optional, and currently has no special semantics. + +Each tag must be sequences of `[a-zA-Z0-9]` separated by `-`, at most 63 +characters in total. + ## Kind: Component Describes the following entity kind: @@ -252,6 +275,8 @@ spec: type: website lifecycle: production owner: artist-relations@example.com + implementsApis: + - artist-api ``` In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) @@ -312,14 +337,22 @@ Apart from being a string, the software catalog leaves the format of this field open to implementers to choose. Most commonly, it is set to the ID or email of a group of people in an organizational structure. +### `spec.implementsApis` [optional] + +Links APIs that are implemented by the component, e.g. `artist-api`. This field +is optional. + +The software catalog expects a list of one or more strings that references the +names of other entities of the `kind` `API`. + ## Kind: Template Describes the following entity kind: -| Field | Value | -| -------------------- | ----------------------- | -| `apiVersion` | `backstage.io/v1alpha1` | -| `Kind: Templatekind` | `Template` | +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Template` | A Template describes a skeleton for use with the Scaffolder. It is used for describing what templating library is supported, and also for documenting the @@ -337,8 +370,8 @@ metadata: description: Next.js application skeleton for creating isomorphic web applications. tags: - - Recommended - - React + - recommended + - react spec: owner: web@example.com templater: cookiecutter @@ -422,3 +455,76 @@ specify relative to the `template.yaml` definition. This is also particularly useful when you have multiple template definitions in the same repository but only a single `template.yaml` registered in backstage. + +## Kind: API + +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `API` | + +An API describes an interface that can be exposed by a component. The API can be +defined in different formats, like [OpenAPI](https://swagger.io/specification/), +[AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/), +[gRPC](https://developers.google.com/protocol-buffers), or other formats. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: artist-api + description: Retrieve artist details +spec: + type: openapi + definition: | + openapi: "3.0.0" + info: + version: 1.0.0 + title: Artist API + license: + name: MIT + servers: + - url: http://artist.spotify.net/v1 + paths: + /artists: + get: + summary: List all artists + ... +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `API`, respectively. + +### `spec.type` [required] + +The type of the API definition as a string, e.g. `openapi`. This field is +required. + +The software catalog accepts any type value, but an organisation should take +great care to establish a proper taxonomy for these. Tools including Backstage +itself may read this field and behave differently depending on its value. For +example, an OpenAPI type API may be displayed using an OpenAPI viewer tooling in +the Backstage interface. + +The current set of well-known and common values for this field is: + +- `openapi` - An API definition in YAML or JSON format based on the + [OpenAPI](https://swagger.io/specification/) version 2 or version 3 spec. +- `asyncapi` - An API definition based on the + [AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/) spec. +- `grpc` - An API definition based on + [Protocol Buffers](https://developers.google.com/protocol-buffers) to use with + [gRPC](https://grpc.io/). + +### `spec.definition` [required] + +The definition of the API, based on the format defined by `spec.type`. This +field is required. diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index e69de29bb2..8b621f2e44 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -0,0 +1,42 @@ +--- +id: extending-the-model +title: Extending the model +--- + +Backstage natively supports tracking of the following component +[`type`](descriptor-format.md)'s: + +- Services +- Websites +- Libraries +- Documentation +- Other + +![](../../assets/software-catalog/bsc-extend.png) + +Since these types are likely not the only kind of software you will want to +track in Backstage, it is possible to + +It is possible to add your own software types that fits your organization's data +model. Inside Spotify our model has grown significantly over the years, and now +includes ML models, Apps, data pipelines and many more. + +## Adding a new type + +TODO: Describe what changes are needed to add a new type that shows up in the +catalog. + +## The Other type + +It might be tempting to put software that doesn't fit into any of the existing +types into Other. There are a few reasons why we advice against this; firstly, +we have found that it is preferred to match the conceptual model that your +engineers have when describing your software. Secondly, Backstage helps your +engineers manage their software by integrating the infrastructure tooling +through plugins. Different plugins are used for managing different types of +components. + +For example, the +[Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse) +only makes sense for Websites. The more specific you can be in how you model +your software, the easier it is to provide plugins that are contextual. diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index e69de29bb2..7ca869cdae 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -0,0 +1,11 @@ +--- +id: external-integrations +title: External integrations +--- + +Backstage natively supports storing software components in +[metadata YAML files](descriptor-format.md). However, companies that already +have an existing system for keeping track of software and its owners can +integrate such systems with Backstage. + +TODO: Describe the API contract. diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index 4925f32e1a..bfc3d53d4d 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -1,4 +1,8 @@ -# Backstage Service Catalog (alpha) +--- +id: software-catalog-overview +title: Backstage Service Catalog (alpha) +sidebar_label: Backstage Service Catalog +--- ## What is a Service Catalog? @@ -6,21 +10,132 @@ The Backstage Service Catalog — actually, a software catalog, since it include more than just services — is a centralized system that keeps track of ownership and metadata for all the software in your ecosystem (services, websites, libraries, data pipelines, etc). The catalog is built around the concept of -[metadata yaml files](../../architecture-decisions/adr002-default-catalog-file-format.md#format) -stored together with the code, which are then harvested and visualized in -Backstage. +[metadata YAML files](descriptor-format.md) stored together with the code, which +are then harvested and visualized in Backstage. ![service-catalog](https://backstage.io/blog/assets/6/header.png) -We have also found that the service catalog is a great way to organise the -infrastructure tools you use to manage the software as well. This is how -Backstage creates one developer portal for all your tools. Rather than asking -teams to jump between different infrastructure UI’s (and incurring additional -cognitive overhead each time they make a context switch), most of these tools -can be organised around the entities in the catalog. +## How it works -## Using the Service Catalog +Backstage and the Backstage Service Catalog makes it easy for one team to manage +10 services — and makes it possible for your company to manage thousands of +them. -TODO +More specifically, the Service Catalog enables two main use-cases: -![](service-catalog-home.png) +1. Helping teams manage and maintain the software they own. Teams get a uniform + view of all their software; services, libraries, websites, ML models — you + name it, Backstage knows all about it. +2. Makes all the software in your company, and who owns it, discoverable. No + more orphan software hiding in the dark corners of your software ecosystem. + +## Getting Started + +The Software Catalog is available to browse on the start page at `/`. If you've +followed [Installing in your Backstage App](./installation.md) in your separate +App or [Getting Started with Backstage](../../getting-started) for this repo, +you should be able to browse the catalog at `http://localhost:3000`. + +![](../../assets/software-catalog/service-catalog-home.png) + +## Adding components to the catalog + +The source of truth for the components in your service catalog are +[metadata YAML files](descriptor-format.md) stored in source control (GitHub, +GitHub Enterprise, GitLab, ...). + +There are 3 ways to add components to the catalog: + +1. Manually register components +2. Creating new components through Backstage +3. Integrating with and [external source](external-integrations.md) + +### Manually register components + +Users can register new components by going to `/create` and clicking the +**REGISTER EXISTING COMPONENT** button: + +![](../../assets/software-catalog/bsc-register-1.png) + +Backstage expects the full URL to the YAML in your source control. Example: + +```bash +https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml +``` + +_More examples can be found +[here](https://github.com/spotify/backstage/tree/master/packages/catalog-model/examples)._ + +![](../../assets/software-catalog/bsc-register-2.png) + +It is important to note that any kind of software can be registered in +Backstage. Even if the software is not maintained by your company (SaaS +offering, for example) it is still useful to create components for tracking +ownership. + +### Creating new components through Backstage + +All software created through the +[Backstage Software Templates](../software-templates/index.md) are automatically +registered in the catalog. + +### Static catalog configuration + +In addition to manually registering components, it is also possible to register +components though [static configuration](../../conf/index.md). For example, the +above example can be added using the following configuration: + +```yaml +catalog: + locations: + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml +``` + +More information about catalog configuration can be found +[here](configuration.md). + +### Updating component metadata + +Teams owning the components are responsible for maintaining the metadata about +them, and do so using their normal Git workflow. + +![](../../assets/software-catalog/bsc-edit.png) + +Once the change has been merged, Backstage will automatically show the updated +metadata in the service catalog after a short while. + +## Finding software in the catalog + +By default the service catalog shows components owned by the team of the logged +in user. But you can also switch to _All_ to see all the components across your +company's software ecosystem. Basic inline _search_ and _column filtering_ makes +it easy to browse a big set of components. + +![](../../assets/software-catalog/bsc-search.png) + +## Starring components + +For easy and quick access to components you visit frequently, Backstage supports +_starring_ of components: + +![](../../assets/software-catalog/bsc-starred.png) + +## Integrated tooling through plugins + +The service catalog is a great way to organize the infrastructure tools you use +to manage the software. This is how Backstage creates one developer portal for +all your tools. Rather than asking teams to jump between different +infrastructure UIs (and incurring additional cognitive overhead each time they +make a context switch), most of these tools can be organized around the entities +in the catalog. + +![tools](https://backstage.io/blog/assets/20-05-20/tabs.png) + +The Backstage platform can be customized by incorporating +[existing open source plugins](https://github.com/spotify/backstage/tree/master/plugins), +or by [building your own](../../plugins/index.md). + +## Links + +- [[Blog post] Backstage Service Catalog released in alpha](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha) diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md new file mode 100644 index 0000000000..397abcc6b0 --- /dev/null +++ b/docs/features/software-catalog/installation.md @@ -0,0 +1,193 @@ +# Installing in your Backstage App + +The catalog plugin comes in two packages, `@backstage/plugin-catalog` and +`@backstage/plugin-catalog-backend`. Each has their own installation steps, +outlined below. + +## Installing @backstage/plugin-catalog + +> **Note that if you used `npx @backstage/create-app`, the plugin may already be +> present** + +The catalog frontend plugin should be installed in your `app` package, which is +created as a part of `@backstage/create-app`. To install the package, run: + +```bash +cd packages/app +yarn add @backstage/plugin-catalog +``` + +Make sure the version of `@backstage/plugin-catalog` matches the version of +other `@backstage` packages. You can update it in `packages/app/package.json` if +it doesn't. + +### Adding the Plugin to your `packages/app` + +Add the following entry to the head of your `packages/app/src/plugins.ts`: + +```ts +export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; +``` + +Add the following to your `packages/app/src/apis.ts`: + +```ts +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; + +// Inside the ApiRegistry builder function ... + +builder.add( + catalogApiRef, + new CatalogClient({ + apiOrigin: backendUrl, + basePath: '/catalog', + }), +); +``` + +Where `backendUrl` is the `backend.baseUrl` from config, i.e. +`const backendUrl = config.getString('backend.baseUrl')`. + +The catalog components depend on a number of other +[Utility APIs](../../api/utility-apis.md) to function, including at least the +`ErrorApi` and `StorageApi`. You can find an example of how to install these in +your app +[here](https://github.com/spotify/backstage/blob/61c3a7e5b750dc7c059ef16b188594d31b2c04c2/packages/app/src/apis.ts#L80). + +## Gotchas that we will fix + +Since the catalog plugin currently ships with a sentry plugin `InfoCard` +installed by default, you'll need to set `sentry.organization` in your +`app-yaml.yaml`. For example: + +```yaml +sentry: + organization: Acme Corporation +``` + +If you've created an app with an older version of `@backstage/create-app` or +`@backstage/cli create-app`, be sure to remove the Welcome plugin from the app, +as that will conflict with the catalog routes. + +## Installing @backstage/plugin-catalog-backend + +> **Note that if you used `npx @backstage/create-app`, the plugin may already be +> present** + +The catalog backend should be installed in your `backend` package, which is +created as a part of `@backstage/create-app`. To install the package, run: + +```bash +cd packages/backend +yarn add @backstage/plugin-catalog-backend +``` + +Make sure the version of `@backstage/plugin-catalog-backend` matches the version +of other `@backstage` packages. You can update it in +`packages/backend/package.json` if it doesn't. + +### Adding the Plugin to your `packages/backend` + +You'll need to add the plugin to the `backend`'s router. You can do this by +creating a file called `packages/backend/src/plugins/catalog.ts` with the +following contents to get you up and running quickly. + +```ts +import { + createRouter, + DatabaseEntitiesCatalog, + DatabaseLocationsCatalog, + DatabaseManager, + HigherOrderOperations, + LocationReaders, + runPeriodically, +} from '@backstage/plugin-catalog-backend'; +import { PluginEnvironment } from '../types'; +import { useHotCleanup } from '@backstage/backend-common'; + +export default async function createPlugin({ + logger, + database, +}: PluginEnvironment) { + const locationReader = new LocationReaders(logger); + + const db = await DatabaseManager.createDatabase(database, { logger }); + const entitiesCatalog = new DatabaseEntitiesCatalog(db); + const locationsCatalog = new DatabaseLocationsCatalog(db); + const higherOrderOperation = new HigherOrderOperations( + entitiesCatalog, + locationsCatalog, + locationReader, + logger, + ); + + useHotCleanup( + module, + runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000), + ); + + return await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger, + }); +} +``` + +Once the `catalog.ts` router setup file is in place, add the router to +`packages/backend/src/index.ts`: + +```ts +import catalog from './plugins/catalog'; + +const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); + +const service = createServiceBuilder(module) + .loadConfig(configReader) + /** several different routers */ + .addRouter('/catalog', await catalog(catalogEnv)); +``` + +### Adding Entries to the Catalog + +At this point the catalog backend is installed in your backend package, but you +will not have any entities loaded. + +To get up and running and try out some templates quickly, you can add some of +our example templates through static configuration. Add the following to the +`catalog.locations` section in your `app-config.yaml`: + +```yaml +catalog: + locations: + # Backstage Example Component + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml +``` + +### Running the Backend + +Finally, start up the backend with the new configuration: + +```bash +cd packages/backend +yarn start +``` + +If you've also set up the frontend plugin, so you should be ready to go browse +the catalog at [localhost:3000](http://localhost:3000) now! diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index e69de29bb2..4436f60fc2 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -0,0 +1,116 @@ +--- +id: system-model +title: System Model +--- + +We believe that a strong shared understanding and terminology around software +and resources leads to a better Backstage experience. + +_This description originates from +[this RFC](https://github.com/spotify/backstage/issues/390). Note that some of +the concepts are not yet supported in Backstage._ + +## Core Entities + +We model software in the Backstage catalogue using these three core entities +(further explained below): + +- **Components** are individual pieces of software + +- **APIs** are the boundaries between different components + +- **Resources** are physical or virtual infrastructure needed to operate a + component + +![](../../assets/software-catalog/software-model-core-entities.png) + +### Component + +A component is a piece of software, for example a mobile feature, web site, +backend service or data pipeline (list not exhaustive). A component can be +tracked in source control, or use some existing open source or commercial +software. + +A component can implement APIs for other components to consume. In turn it might +depend on APIs implemented by other components, or resources that are attached +to it at runtime. + +### API + +APIs form an important (maybe the most important) abstraction that allows large +software ecosystems to scale. Thus, APIs are a first class citizen in the +Backstage model and the primary way to discover existing functionality in the +ecosystem. + +APIs are implemented by components and form boundaries between components. They +might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema (eg +Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by +components need to be in a known machine-readable format so we can build further +tooling and analysis on top. + +APIs have a visibility: they are either public (making them available for any +other component to consume), restricted (only available to a whitelisted set of +consumers), or private (only available within their system). As public APIs are +going to be the primary way interaction between components, Backstage supports +documenting, indexing and searching all APIs so we can browse them as +developers. + +### Resource + +Resources are the infrastructure a component needs to operate at runtime, like +BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together +with components and systems will better allow us to visualize resource +footprint, and create tooling around them. + +## Ecosystem Modeling + +A large catalogue of components, APIs and resources can be highly granular and +hard to understand as a whole. It might thus be convenient to further categorize +these entities using the following (optional) concepts: + +- **Systems** are a collection of entities that cooperate to perform some + function +- **Domains** relate entities and systems to part of the business + +### System + +With increasing complexity in software, systems form an important abstraction +level to help us reason about software ecosystems. Systems are a useful concept +in that they allow us to ignore the implementation details of a certain +functionality for consumers, while allowing the owning team to make changes as +they see fit (leading to low coupling). + +A system, in this sense, is a collection of resources and components that +exposes one or several public APIs. The main benefit of modelling a system is +that it hides its resources and private APIs between the components for any +consumers. This means that as the owner, you can evolve the implementation, in +terms of components and resources, without your consumers being able to notice. +Typically, a system will consist of at most a handful of components (see Domain +for a grouping of systems). + +For example, a playlist management system might encapsulate a backend service to +update playlists, a backend service to query them, and a database to store them. +It could expose an RPC API, a daily snapshots dataset, and an event stream of +playlist updates. + +### Domain + +While systems are the basic level of encapsulation for related entities, it is +often useful to group a collection of systems that share terminology, domain +models, metrics, KPIs, business purpose, or documentation, i.e. they form a +bounded context. + +For example, it would make sense if the different systems in the “Payments” +domain would come with some documentation on how to accept payments for a new +product or use-case, share the same entity types in their APIs, and integrate +well with each other. Other domains could be “Content Ingestion”, “Ads” or +“Search”. + +## Current status + +Backstage currently supports Components and APIs. + +## Links + +- [Original RFC](https://github.com/spotify/backstage/issues/390) +- [YAML file format](../../architecture-decisions/adr002-default-catalog-file-format.md) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index bf540d656a..4f7af7c015 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -1,4 +1,7 @@ -# Adding your own Templates +--- +id: adding-templates +title: Adding your own Templates +--- Templates are stored in the **Service Catalog** under a kind `Template`. The minimum that the a template skeleton needs is a `template.yaml` but it would be @@ -15,11 +18,12 @@ metadata: # title of the template title: React SSR Template # a description of the template - description: Next.js application skeleton for creating isomorphic web applications. + description: + Next.js application skeleton for creating isomorphic web applications. # some tags to display in the frontend tags: - - Recommended - - React + - recommended + - react spec: # which templater key to use in the templaters builder templater: cookiecutter @@ -39,7 +43,7 @@ spec: type: string description: Unique name of the component description: - title: Description + title: Description type: string description: Description of the component ``` @@ -50,11 +54,34 @@ contains more information about the required fields. Once we have a `template.yaml` ready, we can then add it to the service catalog for use by the scaffolder. -Currently the catalog supports loading definitions from Github + Local Files. To +Currently the catalog supports loading definitions from GitHub + Local Files. To load from other places, not only will there need to be another preparer, but the support to load the location will also need to be added to the Catalog. -For loading from a file the following command should work when the backend is +You can add the template files to the catalog through +[static location configuration](../software-catalog/configuration.md#static-location-configuration), +for example + +```yaml +catalog: + locations: + - type: github + target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + rules: + - allow: [Template] +``` + +Templates can also be added by posting the to the catalog directly. Note that if +you're doing this, you need to configure the catalog to allow template entities +to be ingested from any source, for example: + +```yaml +catalog: + rules: + - allow: [Component, API, Template] +``` + +For loading from a file, the following command should work when the backend is running: ```sh @@ -65,7 +92,7 @@ curl \ --data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}" ``` -If loading from a git location, you can run the following +If loading from a Git location, you can run the following ```sh curl \ @@ -79,7 +106,7 @@ This should then have added the catalog, and also should now be listed under the create page at http://localhost:3000/create. Alternatively, if you want to get setup with some mock templates that are -already provided for you, you can run the following to load those templates: +already provided, run the following to load those templates: ``` yarn lerna run mock-data diff --git a/docs/features/software-templates/extending/create-your-own-preparer.md b/docs/features/software-templates/extending/create-your-own-preparer.md index 4e45bef535..0b3fb31323 100644 --- a/docs/features/software-templates/extending/create-your-own-preparer.md +++ b/docs/features/software-templates/extending/create-your-own-preparer.md @@ -1,4 +1,7 @@ -# Create your own Preparer +--- +id: extending-preparer +title: Create your own Preparer +--- Preparers are responsible for reading the location of the definition of a [Template Entity](../../software-catalog/descriptor-format.md#kind-template) and @@ -51,7 +54,7 @@ The `protocol` is set on the when added to the service catalog. You can see more about this `PreparerKey` here in [Register your own template](../adding-templates.md) -**note:** Currently the catalog supports loading definitions from Github + Local +**note:** Currently the catalog supports loading definitions from GitHub + Local Files, which translate into the two `PreparerKeys` `file` and `github`. To load from other places, not only will there need to be another preparer, but the support to load the location will also need to be added to the Catalog. diff --git a/docs/features/software-templates/extending/create-your-own-publisher.md b/docs/features/software-templates/extending/create-your-own-publisher.md index 8baf604e01..391f6f0658 100644 --- a/docs/features/software-templates/extending/create-your-own-publisher.md +++ b/docs/features/software-templates/extending/create-your-own-publisher.md @@ -1,11 +1,14 @@ -# Create your own Publisher +--- +id: extending-publisher +title: Create your own Publisher +--- Publishers are responsible for pushing and storing the templated skeleton after the values have been templated by the `Templater`. See [Create your own templater](./create-your-own-templater.md) for more info. -They recieve a directory or location where the templater has sucessfully run on, -and is now ready to store somewhere. They also get given some other options +They receive a directory or location where the templater has sucessfully run +and is now ready to store somewhere. They also are given some other options which are sent from the frontend, such as the `storePath` which is a string of where the frontend thinks we should save this templated folder. @@ -14,7 +17,7 @@ Currently we provide the following `publishers`: - `github` This publisher is passed through to the `createRouter` function of the -`@spotify/plugin-scaffolder-backend`. Currently only one publisher is supported, +`@spotify/plugin-scaffolder-backend`. Currently, only one publisher is supported, but PR's are always welcome. An full example backend can be found diff --git a/docs/features/software-templates/extending/create-your-own-templater.md b/docs/features/software-templates/extending/create-your-own-templater.md index 947190c5e5..9dee66af06 100644 --- a/docs/features/software-templates/extending/create-your-own-templater.md +++ b/docs/features/software-templates/extending/create-your-own-templater.md @@ -1,18 +1,21 @@ -# Creating your own Templater +--- +id: extending-templater +title: Creating your own Templater +--- Templaters are responsible for taking the directory path for the skeleton returned by the preparers, and then executing the templating command on top of the file and returning the completed template path. This may or may not be the same directory as the input directory. -They also recieve additional values from the frontend, which can be used to +They also receive additional values from the frontend, which can be used to interpolate into the skeleton files. Currently we provide the following templaters: - `cookiecutter` -This templater is added the `TemplaterBuilder` and then passed into the +This templater is added to the `TemplaterBuilder` and then passed into the `createRouter` function of the `@spotify/plugin-scaffolder-backend` An full example backend can be found @@ -45,7 +48,7 @@ This `TemplaterKey` is used to select the correct templater from the `spec.templater` in the [Template Entity](../../software-catalog/descriptor-format.md#kind-template). -If you wish to add a new templater you'll need to register it with the +If you wish to add a new templater, you'll need to register it with the `TemplaterBuilder`. ### Creating your own Templater to add to the `TemplaterBuilder` @@ -80,10 +83,10 @@ follows: - `dockerClient` - a [dockerode](https://github.com/apocas/dockerode) client to be able to run docker containers. -_note_ currently the templaters that we provide are basically docker action +_note_ Currently the templaters that we provide are basically Docker action containers that are run on top of the skeleton folder. This keeps dependencies to a minimal for running backstage scaffolder, but you don't /have/ to use -docker. You could create your own templater that spins up an EC2 instance and +Docker. You could create your own templater that spins up an EC2 instance and downloads the folder and does everything using an AMI if you want. It's entirely up to you! @@ -113,8 +116,8 @@ metadata: description: Next.js application skeleton for creating isomorphic web applications. tags: - - Recommended - - React + - recommended + - react spec: owner: web@example.com templater: handlebars @@ -135,7 +138,7 @@ spec: description: Description of the component ``` -You see that the `spec.templater` is set as `handlebars`, you'll need to +You see that the `spec.templater` is set as `handlebars`, so you'll need to register this with the `TemplaterBuilder` like so: ```ts diff --git a/docs/features/software-templates/extending/index.md b/docs/features/software-templates/extending/index.md index a55128c57d..02d27a5725 100644 --- a/docs/features/software-templates/extending/index.md +++ b/docs/features/software-templates/extending/index.md @@ -1,12 +1,15 @@ -## Extending the Scaffolder +--- +id: extending-index +title: Extending the Scaffolder +--- Welcome. Take a seat. You're at the Scaffolder Documentation. -So - You wanna create stuff inside your company from some prebaked templates? +So, you want to create stuff inside your company from some prebaked templates? You're at the right place. -This guide is gonna take you through how the Scaffolder in Backstage works. -We'll dive into some jargon and run through whats going on in the backend to be +This guide is going to take you through how the Scaffolder in Backstage works. +We'll dive into some jargon and run through what's going on in the backend to be able to create these templates. There's also more guides that you might find useful at the bottom of this document. At it's core, theres 3 simple stages. @@ -22,9 +25,9 @@ scaffolder that you will need to know: 3. Publish Each of these steps can be configured for your own use case, but we provide some -sensible defaults too. +sensible defaults, too. -Lets dive a little deeper into these phases. +Let's dive a little deeper into these phases. ### Glossary and Jargon @@ -35,8 +38,8 @@ the router to pick the correct `Preparer` to run for the `Template` entity. **Templater** - The templater is responsible for actually running the chosen templater on top of the previously returned temporary directory from the -**Preprarer**. We advise making these docker containers as it can keep all -dependencies, for example Cookiecutter, self contained and not a dependency on +**Preprarer**. We advise making these Docker containers as it can keep all +dependencies--for example Cookiecutter--self contained and not a dependency on the host machine. **Publisher** - The publisher is responsible for taking the finished directory, @@ -47,11 +50,11 @@ passed through to the scaffolder backend. ### How it works -The main of the heavy lifting is done in the +Most of the heavy lifting is done in the [router.ts](https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/service/router.ts#L93) file in the `scaffolder-backend` plugin. -There are 2 routes defined in the router. `POST /v1/jobs` and +There are two routes defined in the router: `POST /v1/jobs` and `GET /v1/job/:jobId` To create a scaffolding job, a JSON object containing the @@ -75,7 +78,7 @@ additional templating values must be posted as the post body. The values should represent something that is valid with the `schema` part of the [Template Entity](../../software-catalog/descriptor-format.md#kind-template) -Once that has been posted, a job will be setup with different stages. And the +Once that has been posted, a job will be setup with different stages, and the job processor will complete each stage before moving onto the next stage, whilst collecting logs and mutating the running job. diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index c7358518cd..1c67676b3e 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -1,19 +1,27 @@ -# Software Templates +--- +id: software-templates-index +title: Software Templates +--- The Software Templates part of Backstage is a tool that can help you create -Components inside Backstage. It by default has the ability to load skeletons of -code, template in some variables and then publish the template to some location +Components inside Backstage. By default, it has the ability to load skeletons of +code, template in some variables, and then publish the template to some location like GitHub. + + ### Getting Started -The Software Templates are available under `/create`, and if you've followed -[Getting Started with Backstage](../../getting-started), you should be able to -reach `http://localhost:3000/create`. +The Software Templates are available under `/create`. If you've followed +[Installing in your Backstage App](./installation.md) in your separate App or +[Getting Started with Backstage](../../getting-started) for this repo, you +should be able to reach `http://localhost:3000/create`. You should get something that looks similar to this: -![Create Image](./assets/create.png) +![Create Image](../../assets/software-templates/create.png) ### Choose a template @@ -22,38 +30,38 @@ page which may or may not look different for each template. Each template can ask for different input variables, and they are then passed to the templater internally. -![Enter some variables](./assets/template-picked.png) +![Enter some variables](../../assets/software-templates/template-picked.png) After filling in these variables, you'll get some more fields to fill out which -are required for backstage usage. The owner, which is a `user` in the backstage -system, and the `storePath` which right now must be a Github Organisation and a -non-existing github repository name in the format `organistaion/reponame`. +are required for backstage usage: the owner, (which is a `user` in the backstage +system), the `storePath` (which right now must be a GitHub Organisation), and a +non-existing github repository name in the format `organisation/reponame`. -![Enter backstage vars](./assets/template-picked-2.png) +![Enter backstage vars](../../assets/software-templates/template-picked-2.png) ### Run! Once you've entered values and confirmed, you'll then get a modal with live progress of what is currently happening with the creation of your template. -![Templating Running](./assets/running.png) +![Templating Running](../../assets/software-templates/running.png) It shouldn't take too long, and you'll have a success screen! -![Templating Complete](./assets/complete.png) +![Templating Complete](../../assets/software-templates/complete.png) If it fails, you'll be able to click on each section to get the log from the -step that failed which can be helpful to debug. +step that failed which can be helpful in debugging. -![Templating failed](./assets/failed.png) +![Templating failed](../../assets/software-templates/failed.png) ### View Component in Catalog -When it's been created you'll see the `View in Catalog` button, which will take +When it's been created, you'll see the `View in Catalog` button, which will take you to the registered component in the catalog: -![Catalog](./assets/go-to-catalog.png) +![Catalog](../../assets/software-templates/go-to-catalog.png) And then you'll also be able to see it in the Catalog View table -![Catalog](./assets/added-to-the-catalog-list.png) +![Catalog](../../assets/software-templates/added-to-the-catalog-list.png) diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md new file mode 100644 index 0000000000..73b4f20359 --- /dev/null +++ b/docs/features/software-templates/installation.md @@ -0,0 +1,194 @@ +# Installing in your Backstage App + +The scaffolder plugin comes in two packages, `@backstage/plugin-scaffolder` and +`@backstage/plugin-scaffolder-backend`. Each has their own installation steps, +outlined below. + +The Scaffolder plugin also depends on the Software Catalog. Instructions for how +to set that up can be found [here](../software-catalog/installation.md). + +## Installing @backstage/plugin-scaffolder + +> **Note that if you used `npx @backstage/create-app`, the plugin may already be +> present** + +The scaffolder frontend plugin should be installed in your `app` package, which +is created as a part of `@backstage/create-app`. To install the package, run: + +```bash +cd packages/app +yarn add @backstage/plugin-scaffolder +``` + +Make sure the version of `@backstage/plugin-scaffolder` matches the version of +other `@backstage` packages. You can update it in `packages/app/package.json` if +it doesn't. + +### Adding the Plugin to your `packages/app` + +Add the following entry to the head of your `packages/app/src/plugins.ts`: + +```ts +export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; +``` + +Add the following to your `packages/app/src/apis.ts`: + +```ts +import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder'; + +// Inside the ApiRegistry builder function ... + +builder.add( + scaffolderApiRef, + new ScaffolderApi({ + apiOrigin: backendUrl, + basePath: '/scaffolder/v1', + }), +); +``` + +Where `backendUrl` is the `backend.baseUrl` from config, i.e. +`const backendUrl = config.getString('backend.baseUrl')`. + +This is all that is needed for the frontend part of the Scaffolder plugin to +work! + +## Installing @backstage/plugin-scaffolder-backend + +> **Note that if you used `npx @backstage/create-app`, the plugin may already be +> present** + +The scaffolder backend should be installed in your `backend` package, which is +created as a part of `@backstage/create-app`. To install the package, run: + +```bash +cd packages/backend +yarn add @backstage/plugin-scaffolder-backend +``` + +Make sure the version of `@backstage/plugin-scaffolder-backend` matches the +version of other `@backstage` packages. You can update it in +`packages/backend/package.json` if it doesn't. + +### Adding the Plugin to your `packages/backend` + +You'll need to add the plugin to the `backend`'s router. You can do this by +creating a file called `packages/backend/src/plugins/scaffolder.ts` with the +following contents to get you up and running quickly. + +```ts +import { + CookieCutter, + createRouter, + FilePreparer, + GithubPreparer, + Preparers, + GithubPublisher, + CreateReactAppTemplater, + Templaters, +} from '@backstage/plugin-scaffolder-backend'; +import { Octokit } from '@octokit/rest'; +import type { PluginEnvironment } from '../types'; +import Docker from 'dockerode'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + const cookiecutterTemplater = new CookieCutter(); + const craTemplater = new CreateReactAppTemplater(); + const templaters = new Templaters(); + + // Register default templaters + templaters.register('cookiecutter', cookiecutterTemplater); + templaters.register('cra', craTemplater); + + const filePreparer = new FilePreparer(); + const githubPreparer = new GithubPreparer(); + const preparers = new Preparers(); + + // Register default preparers + preparers.register('file', filePreparer); + preparers.register('github', githubPreparer); + + // Create GitHub client with your access token from environment variables + const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); + const publisher = new GithubPublisher({ client: githubClient }); + + const dockerClient = new Docker(); + return await createRouter({ + preparers, + templaters, + publisher, + logger, + dockerClient, + }); +} +``` + +Once the `scaffolder.ts` router setup file is in place, add the router to +`packages/backend/src/index.ts`: + +```ts +import scaffolder from './plugins/scaffolder'; + +const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); + +const service = createServiceBuilder(module) + .loadConfig(configReader) + /** several different routers */ + .addRouter('/scaffolder', await scaffolder(scaffolderEnv)); +``` + +### Adding Templates + +At this point the scaffolder backend is installed in your backend package, but +you will not have any templates available to use. These need to be added to the +software catalog, as they are represented as entities of kind +[Template](../software-catalog/descriptor-format.md#kind-template). You can find +out more about adding templates [here](./adding-templates.md). + +To get up and running and try out some templates quickly, you can add some of +our example templates through static configuration. Add the following to the +`catalog.locations` section in your `app-config.yaml`: + +```yaml +catalog: + locations: + # Backstage Example Templates + - type: github + target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml + - type: github + target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml + - type: github + target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml +``` + +### Runtime Dependencies + +For the scaffolder backend plugin to function, it needs a GitHub access token, +and access to a running Docker daemon. You can create a GitHub access token +[here](https://github.com/settings/tokens/new), select `repo` scope only. Full +docs on creating private GitHub access tokens is available +[here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). +Note that the need for private GitHub access tokens will be replaced with GitHub +Apps integration further down the line. + +> **Right now it is only possible to scaffold repositories inside GitHub +> organizations, and not under personal accounts.** + +The GitHub access token is passed along using the `GITHUB_ACCESS_TOKEN` +environment variable. + +### Running the Backend + +Finally, make sure you have a local Docker daemon running, and start up the +backend with the new configuration: + +```bash +cd packages/backend +GITHUB_ACCESS_TOKEN= yarn start +``` + +If you've also set up the frontend plugin, so you should be ready to go browse +the templates at [localhost:3000/create](http://localhost:3000/create) now! diff --git a/docs/features/techdocs/FAQ.md b/docs/features/techdocs/FAQ.md index f872e63718..1a52a35cfb 100644 --- a/docs/features/techdocs/FAQ.md +++ b/docs/features/techdocs/FAQ.md @@ -1,8 +1,13 @@ -# TechDocs FAQ +--- +id: faqs +title: TechDocs FAQ +sidebar_label: FAQ +--- This page answers frequently asked questions about [TechDocs](README.md). -_Got a question that you think others might be interested in knowing the answer to? Edit this file +_Got a question that you think others might be interested in knowing the answer +to? Edit this file [here](https://github.com/spotify/backstage/edit/master/docs/features/techdocs/FAQ.md)._ ## Technology @@ -25,4 +30,3 @@ package is a MkDocs Plugin that works like a wrapper around multiple MkDocs plugins (e.g. [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)) as well as a selection of Python Markdown extensions that TechDocs supports. - diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index d054483482..8bdbd27d3c 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -1,4 +1,8 @@ -# TechDocs Documentation +--- +id: techdocs-overview +title: TechDocs Documentation +sidebar_label: Overview +--- ## What is it? @@ -16,9 +20,9 @@ Spotify’s developer experience offering with 2,400+ documentation sites and - A clear end-to-end docs-like-code solution. (_Coming soon in V.1_) - A tightly coupled feedback loop with the developer workflow. (_Coming soon in - V.2_) + V.3_) -- A developer ecosystem for creating extensions. (_Coming soon in V.2_) +- A developer ecosystem for creating extensions. (_Coming soon in V.3_) ## Project roadmap @@ -26,12 +30,15 @@ Spotify’s developer experience offering with 2,400+ documentation sites and | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | [TechDocs V.0 ✅][v0] | Read docs in Backstage - Enable anyone to get a reader experience working in Backstage. [See V.0 Use Cases.](./#techdocs-v0) | | [TechDocs V.1 🚧][v1] | TechDocs end to end (alpha) - Alpha of TechDocs that you can use end to end - and contribute to. [See V.1 Use Cases.](./#techdocs-v1) | -| [TechDocs V.2 🔮⌛][v2] | Widget Architecture - TechDocs widget architecture available, so the community can create their own customized features. | +| [TechDocs V.2 🔮⌛][v2] | Platform stability and compatibility improvements. [See V.2 Use Cases.](./#techdocs-v2) | +| TechDocs V.3 🔮⌛ | Widget Architecture - TechDocs widget architecture available, so the community can create their own customized features. | [v0]: https://github.com/spotify/backstage/milestone/15 [v1]: https://github.com/spotify/backstage/milestone/16 [v2]: https://github.com/spotify/backstage/milestone/17 + + ## Use Cases #### TechDocs V.0 @@ -56,6 +63,14 @@ Spotify’s developer experience offering with 2,400+ documentation sites and #### TechDocs V.2 +Platform stability and compatibility improvements + +- As a user I can define the metadata generated for my documentation. +- As a user I will be able to browse metadata from within my documentation in + Backstage. + +#### TechDocs V.3 + more to come... ## Structure @@ -86,3 +101,7 @@ more to come... https://github.com/spotify/backstage/blob/master/packages/techdocs-container [techdocs/cli]: https://github.com/spotify/backstage/blob/master/packages/techdocs-cli + +## TechDocs Big Picture + +![TechDocs Big Picture](../..//assets/techdocs/techdocs_big_picture.png) diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index f06ed8b4c0..2fa84cebd6 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -1,6 +1,10 @@ -# Concepts +--- +id: concepts +title: Concepts +--- -This page describes concepts that are introduced with Spotify's docs-like-code solution in Backstage. +This page describes concepts that are introduced with Spotify's docs-like-code +solution in Backstage. ### TechDocs Core Plugin @@ -8,7 +12,7 @@ The TechDocs Core Plugin is a MkDocs plugin created as a wrapper around multiple MkDocs plugins and Python Markdown extensions to standardize the configuration of MkDocs used for TechDocs. -[TechDocs Core](../../../packages/techdocs-container/techdocs-core/README.md) +[TechDocs Core](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/techdocs-core/README.md) ### TechDocs container @@ -17,7 +21,7 @@ The TechDocs container is a Docker container available at pages, including stylesheets and scripts from Python flavored Markdown, through MkDocs. -[TechDocs Container](../../../packages/techdocs-container/README.md) +[TechDocs Container](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/README.md) ### TechDocs publisher (coming soon) @@ -28,7 +32,7 @@ documentation for publishing. Currently it mostly acts as a wrapper around the TechDocs container and provides an easy-to-use interface for our docker container. -[TechDocs CLI](../../../packages/techdocs-cli/README.md) +[TechDocs CLI](https://github.com/spotify/backstage/blob/master/packages/techdocs-cli/README.md) ### TechDocs Reader @@ -38,9 +42,9 @@ sites with the Backstage UI. The TechDocs Reader purpose is also to open up the opportunity to integrate TechDocs widgets for a customized full-featured TechDocs experience. -([coming soon V.2](https://github.com/spotify/backstage/milestone/17)) +([coming soon V.3](./README.md#project-roadmap)) -[TechDocs Reader](../../../plugins/techdocs/src/reader/README.md) +[TechDocs Reader](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/README.md) ### Transformers @@ -49,4 +53,4 @@ Reader. The reason why transformers were introduced was to provide a way to transform the HTML content on pre and post render (e.g. rewrite docs links or modify css). -[Transformers API docs](../../../plugins/techdocs/src/reader/transformers/README.md) +[Transformers API docs](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/README.md) diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index b38fb77b08..037c4be172 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -1,24 +1,48 @@ -# Creating and publishing your docs +--- +id: creating-and-publishing +title: Creating and publishing your docs +sidebar_label: Creating and Publishing Documentation +--- This section will guide you through: -- Creating a basic setup for your documentation -- Writing and previewing your documentation in a local Backstage environment -- Creating a build ready for publication -- Publishing your documentation and making your Backstage instance read your published docs. +- [Create a basic documentation setup](#create-a-basic-documentation-setup) + - [Use the documentation template](#use-the-documentation-template) + - [Manually add documentation setup to already existing repository](#manually-add-documentation-setup-to-already-existing-repository) +- [Writing and previewing your documentation](#writing-and-previewing-your-documentation) ## Prerequisities -- [Docker](https://docs.docker.com/get-docker/) -- Static file hosting -- A working Backstage instance with TechDocs installed - (see [TechDocs getting started](getting-started.md)) +- A working Backstage instance with TechDocs installed (see + [TechDocs getting started](getting-started.md)) ## Create a basic documentation setup -In your home directory (also known as `~`), create a directory that contains your documentation (for example, `hello-docs`). Inside this directory, create a file called `mkdocs.yml`. Below is a basic example of how it could look. +### Use the documentation template -The `~/hello-docs/mkdocs.yml` file should have the following content: +Your working Backstage instance should by default have a documentation template +added. If not, follow these +[instructions](../software-templates/installation.md#adding-templates) to add +the documentation template. + +![Documentation Template](../../assets/techdocs/documentation-template.png) + +Create an entity from the documentation template and you will get the needed +setup for free. + +!!! warning Currently the Backstage Software Templates are limited to create repositories +inside GitHub organizations. You also need to generate an personal access token +and use as an environment variable. Read more about this +[here](../software-templates/installation.md#runtime-dependencies). + +### Manually add documentation setup to already existing repository + +Prerequisities: + +- `catalog-info.yml` file registered to Backstage. + +Create a `mkdocs.yml` file in the root of the repository with the following +content: ```yaml site_name: 'example-docs' @@ -30,7 +54,20 @@ plugins: - techdocs-core ``` -The `~/hello-docs/docs/index.md` should have the following content: +Update your `catalog-info.yaml` file in the root of the repository with the +following content: + +```yaml +metadata: + annotations: + backstage.io/techdocs-ref: dir:./ +``` + +Create a `/docs` folder in the root of the project with at least a `index.md` +file. _(If you add more markdown files, make sure to update the nav in the +mkdocs.yml file to get a proper navigation for your documentation.)_ + +The `docs/index.md` can for example have the following content: ```md # example docs @@ -38,6 +75,9 @@ The `~/hello-docs/docs/index.md` should have the following content: This is a basic example of documentation. ``` +Commit your changes, open a pull request and merge. You will now get your +updated documentation next time you run Backstage! + ## Writing and previewing your documentation Using the `techdocs-cli` you can preview your docs inside a local Backstage @@ -47,77 +87,6 @@ want to write your documentation. To do this you can run: ```bash -cd ~/hello-docs/ +cd ~// npx techdocs-cli serve ``` - -## Build production ready documentation - -To get a build suitable for publication you can build your docs using the -`spotify/techdocs` container: - -```bash -cd ~/hello-docs/ -docker run -it -w /content -v $(pwd):/content spotify/techdocs build -``` - -You should now have a folder called `~/hello-docs/site/`. - -## Deploy to a file server - -In order to serve documentation to TechDocs, our Backstage plugin needs to download the HTML rendered from the previous step. This will likely exist on an external file server, or a storage solution such as Google Cloud Storage. - -When deploying documentation, it should be deployed on that file server/storage solution with the following convention: `{id}/{file}`. For example, if -you want to upload the `getting-started/index.html` file for the `backstage` -documentation site, we would upload it to our file server as -`backstage/getting-started/index.html`. - -To explain further what this would look like for multiple documentation sites, -take a look at this example file tree that would be represented on your file server: - -```md -/backstage/index.html /backstage/getting-started/index.html -/backstage/contributing/index.html /mkdocs/index.html -/mkdocs/plugin-development/index.html -/mkdocs/plugin-development/debugging/index.html -``` - -In this file tree, we have two documentation sites available: `backstage` and -`mkdocs`. Each of them expose several pages. Let's say both of these are hosted -on `http://example.com` as the server URL. - -When you configure the TechDocs plugin in Backstage to use `http://example.com` -as the file server/storage solution, it will translate the following URLs to -the file server: - -| Backstage URL | File Server URL | -| --------------------------------------------------------- | ------------------------------------------------------- | -| https://demo.backstage.io/docs/backstage/ | http://example.com/backstage/index.html | -| https://demo.backstage.io/docs/mkdocs/plugin-development/ | http://example.com/mkdocs/plugin-development/index.html | - -Then deploying new sites is easy: simply copy over the `site/` -folder produced in the [Create documentation](#build-production-ready-documentation) step above to the file server/storage solution under the ID of the documentation site. It will then become immediately available in Backstage under -the same ID as you can see in the table above. - -So, if the URL to your file server is `http://example.com/`, your -`~/hello-docs/site` folder containing the documentation should be accessible at -`http://example.com/hello-docs/`. - -## Configure TechDocs to read from file server - -In order for Backstage to show your documentation, it needs to know where you -uploaded it. - -Make sure you have Backstage set up using -[TechDocs getting started](getting-started.md). - -To point Backstage to your docs storage, add or change the following lines in -your Backstage `app-config.yaml`: - -```yaml -techdocs: - storageUrl: http://example.com -``` - -You can now start Backstage using `yarn start` and open up your browser at -`http://localhost:3000/docs/hello-docs` to view your docs. diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 90dcd423fa..414b3baae8 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -1,17 +1,10 @@ -# Getting Started +--- +id: getting-started +title: Getting Started +--- -> TechDocs is not yet feature complete - currently you can't set up a complete -> end-to-end working TechDocs plugin without customizing the plugin itself. - -> What you can expect from TechDocs V.0 is a demonstration of how to integrate docs into -> Backstage. TechDocs can create docs using -> [mkdocs](https://www.mkdocs.org/), as well as read published docs. If you -> publish generated docs and pass in a `storageUrl` in your `app-config.yaml`, -> you can view them in Backstage by going to -> `http://localhost:3000/docs/`. - -TechDocs functions as a plugin to -Backstage, so you will need to use Backstage to use TechDocs. +TechDocs functions as a plugin to Backstage, so you will need to use Backstage +to use TechDocs. ## What is Backstage? @@ -38,17 +31,20 @@ To create a new Backstage application for TechDocs, run the following command: npx @backstage/cli create-app ``` -You will then be prompted to enter a name for your application. Once that's done, a new Backstage application will be created in a new folder. For -example, if you choose the name `hello-world`, a new `hello-world` folder is created containing your new Backstage application. +You will then be prompted to enter a name for your application. Once that's +done, a new Backstage application will be created in a new folder. For example, +if you choose the name `hello-world`, a new `hello-world` folder is created +containing your new Backstage application. ## Installing TechDocs -TechDocs is not provided with the Backstage application by default, so you will now need to set up TechDocs manually. It should take less -than a minute. +TechDocs is provided with the Backstage application by default. If you want to +set up TechDocs manually, keep follow the instructions below. ### Adding the package -The first step is to add the TechDocs plugin to your Backstage application. Navigate to your new Backstage application folder: +The first step is to add the TechDocs plugin to your Backstage application. +Navigate to your new Backstage application folder: ```bash cd hello-world/ @@ -61,7 +57,7 @@ cd packages/app yarn add @backstage/plugin-techdocs ``` -After a short while, the TechDocs plugin should be successfully installed. +After a short while, the TechDocs plugin should be successfully installed. Next, you need to set up some basic configuration. Enter the following command: @@ -78,18 +74,29 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs'; ### Setting the configuration TechDocs allows for configuration of the docs storage URL through your -`app-config` file. The URL provided here is for demo docs to use for testing purposes. +`app-config` file. -To use the demo docs, add the following lines to `app-config.yaml`: +The default storage URL: ```yaml techdocs: - storageUrl: https://techdocs-mock-sites.storage.googleapis.com + storageUrl: http://localhost:7000/techdocs/static/docs ``` +If you want to configure this to point to another storage URL, change the value +of `storageUrl`. + ## Run Backstage locally -Change folder to your Backstage application root and run the following command: +Change folder to `/packages/backend` and run the +following command: + +```bash +yarn start +``` + +Open a new command line window. Change directory to your Backstage application +root and run the following command: ```bash yarn start @@ -99,5 +106,5 @@ Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/). ## Additional reading - * [Creating and publishing your docs](creating-and-publishing.md) - * [Back to README](README.md) +- [Creating and publishing your docs](creating-and-publishing.md) +- [Back to README](README.md) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 8c8b150171..e555c6b52d 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -1,4 +1,7 @@ -# Custom App Themes +--- +id: app-custom-theme +title: Customize the look-and-feel of your App +--- Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index e69de29bb2..a0d2c27f89 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -0,0 +1,6 @@ +--- +id: configure-app-with-plugins +title: Configuring App with plugins +--- + +Coming soon! diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index ab5e71ad60..fe22935002 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -1,4 +1,7 @@ -# Backstage App +--- +id: create-an-app +title: Create an App +--- To get set up quickly with your own Backstage project you can create a Backstage App. @@ -22,7 +25,7 @@ This will create a new Backstage App inside the current folder. The name of the app-folder is the name that was provided when prompted.

- create app + create app

Inside that directory, it will generate all the files and folder structure diff --git a/docs/getting-started/deployment-k8s.md b/docs/getting-started/deployment-k8s.md index e69de29bb2..2a184779e2 100644 --- a/docs/getting-started/deployment-k8s.md +++ b/docs/getting-started/deployment-k8s.md @@ -0,0 +1,6 @@ +--- +id: deployment-k8s +title: Kubernetes +--- + +Coming soon! diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 26c70acac4..3b7796f8fe 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -1,4 +1,7 @@ -# Deployment (Other) +--- +id: deployment-other +title: Other +--- ## Deploying Locally diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index e10ee4b24a..421ce6d2c5 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -1,4 +1,7 @@ -# Development Environment +--- +id: development-environment +title: Development Environment +--- This section describes how to get set up for doing development on the Backstage repository. @@ -82,7 +85,3 @@ yarn create-plugin # Create a new plugin > See > [package.json](https://github.com/spotify/backstage/blob/master/package.json) > for other yarn commands/options. - -[Next Step - Create a Backstage plugin](../plugins/create-a-plugin.md) - -[Back to Docs](../README.md) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 2770b7a154..abe7776371 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,10 +1,24 @@ -# Getting started with Backstage +--- +id: index +title: Running Backstage Locally +--- -## Running Backstage Locally +First make sure you are using NodeJS with an Active LTS Release, currently v12. +This is made easy with a version manager such as nvm which allows for version switching. + +```bash +# Checking your version +node --version +> v14.7.0 + +# Adding a second node version +nvm install 12 +> Downloading and installing node v12.18.3... +> Now using node v12.18.3 (npm v6.14.6) +``` To get up and running with a local Backstage to evaluate it, let's clone it off -of GitHub and run an initial build. First make sure that you have at least node -version 12 installed locally. +of GitHub and run an initial build. ```bash # Start from your local development folder diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index e69de29bb2..a4f91b6e95 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -0,0 +1,6 @@ +--- +id: installation +title: Installation +--- + +Coming soon! diff --git a/docs/overview/adopting.md b/docs/overview/adopting.md new file mode 100644 index 0000000000..893eac6398 --- /dev/null +++ b/docs/overview/adopting.md @@ -0,0 +1,160 @@ +--- +id: adopting +title: Strategies for adopting +--- + +This document outlines some general best practices that have been key to +Backstage's success inside Spotify. Every organization is different and some of +these learnings will therefore not be applicable for your company. We are hoping +that this can become a living document, and strongly encourage you to contribute +back whatever learnings you gather while adopting Backstage inside your company. + +## Organizational setup + +The true value of Backstage is unlocked when it becomes _THE_ developer portal +at your company. As such it is important to recognize that you will need a +central team that owns your Backstage deployment and treats it like a product. + +This team will have **four** primary objectives: + +1. Maintain and operate your deployment of Backstage. This includes customer + support, infrastructure, CI/CD and, as your Backstage product grows, on-call + support. + +2. Drive adoption of customers (developers at your company). + +3. Work with senior tech leadership and architects to ensure your organizations + best practices for software development are encoded into a set of + [Software Templates](../features/software-templates/index.md). + +4. Evangelize Backstage as a central platform towards other + infrastructure/platform teams. + +## Internal evangelization + +The last objective deserves more attention, since it is the least obvious, but +also the most critical to successfully creating a consolidated platform. When +done right, Backstage acts as a "platform of platforms" or marketplace between +infra/platform teams and end-users: + +![pop](../assets/pop.png) + +While anyone at your company can contribute to the platform, the vast majority +of work will be done by teams that also has internal engineers as their +customers. The central team should treat these _contributing teams_ as customers +of the platform as well. + +These teams should be able to autonomously deliver value directly to their +customers. This is done primarily by building [plugins](../plugins/index.md). +Contributing teams should themselves treat their plugins as, or part of, the +products they maintain. + +> Case study: Inside Spotify we have a team that owns our CI platform. They +> don't only maintain the pipelines and build servers, but also expose their +> product in Backstage through a plugin. Since they also +> [maintain their own API](../plugins/call-existing-api.md), they can improve +> their product by iterating on API and UI in lockstep. Because the plugin +> follows our [platform design guidelines](../dls/design.md) their customers get +> a CI experience that is consistent with other tools on the platform (and users +> don't have to become experts in Jenkins). + +### Tactics + +Example of tactics we have used to evangelize Backstage internally: + +- Arrange "Lunch & Learns" and seminars. Frequently offer teams interested in + Backstage development to come to a seminar where you show, for example, how to + build a plugin from scratch. + +- Embedding. As contributing teams start development of their first plugin it is + often very appreciated to have one person from the central team come over and + "embed" for a Sprint or two. + +- Hack days. Backstage-focused Hackathons or hack days is a fun way to get + people into plugin development. + +- Show & tell meetings. In order to build an internal community around Backstage + we have quarterly meetings where anyone working on Backstage is invited to + present their work. This is a not only a great way to get early feedback, but + also helps coordination between teams that are building overlapping + experiences. + +- Provide metrics. Add instrumentation to your Backstage deployment and make + metrics available to contributing teams. At Spotify we have even gone so far + as sending out weekly digest email showing how usage metrics have changed for + individual plugins. + +- Pro-actively identify new plugins. Reach out to teams that own internal UIs or + platforms that you think would make sense to consolidate into Backstage. + +## KPIs and metrics + +These are some of the metrics that you can use to verify if Backstage has a +successful impact on your software development process: + +- **Onboarding time** Time until new engineers are productive. At Spotify we + measure this as the time until the employee has merged their 10th PR (this + metric was down 55% two years after deploying Backstage). Even though you may + not be onboarding engineers at a rapid pace, this metric is a great proxy for + the overall complexity of your ecosystem. Reducing it will therefore benefit + your whole engineering organization, not just new joiners. + +- **Number of merges per developer/day** Less time spent jumping between + different tools and looking for information means more time to focus on + shipping code. A second level of bottlenecks can be identified if you + categorize contributions by domain (services, web, data, etc). + +- **Deploys to production** Cousin to the metric above: How many times does an + engineer push changes into production. + +- **MTTR** With clear ownership of all the pieces in your micro services + ecosystem and all tools integrated into one place, Backstage makes it quicker + for teams to find the root cause of failures, and fix them. + +- **Context switching** Reducing context switching can help engineers stay in + the "zone". We measure the number of different tools an engineer have to + interact with in order to get a certain job done (e.g. push a change, follow + it into production and validate it did not break anything). + +- **T-shapedness** A + [T-shaped](https://medium.com/@jchyip/why-t-shaped-people-e8706198e437) + engineer is someone that is able to contribute to different domains of + engineering. Teams with T-shaped people have fewer bottlenecks and can + therefore deliver more consistently. Backstage makes it easier to be T-shaped + since tools and infrastructure is consistent between domains, and information + is available centrally. + +- **eNPS** Surveys asking about how productive people feel, how easy it is to + find information and overall satisfaction with internal tools. + +- **Fragmentation** _(Experimental)_ Backstage + [Software Templates](../features/software-templates/index.md) helps drive + standardization in your software ecosystem. By measuring the variance in + technology between different software components it is possible to get a sense + of the overall fragmentation in your ecosystem. Examples could include: + framework versions, languages, deployment methods and various code quality + measurements. + +Additionally, these proxy metrics can be used to validate the success of +Backstage as _the_ platform: + +- Nr of teams that have contributed at least one plugin (currently 63 inside + Spotify) + +- Nr of total plugins (currently 135 inside Spotify) + +- % of contributions coming from outside the central Backstage team (currently + 85% inside Spotify) + +- Traditional metrics such as visits (MAU, DAU, etc) and page views. Currently + ~50% of all Spotifiers use Backstage on a monthly basis, even though the + percentage of engineers is below 50%. Most engineers actually use Backstage on + a daily basis. + +Again, any feedback is appreciated. Please use the Edit button at the top of the +page to make a suggestion. + +_**Note!** It might be tempting to try to optimize Backstage usage and +"engagement". Even though you want to consolidate all your tooling and technical +documentation in Backstage, it is important to remember that time spent in +Backstage is time not spent writing code_ 🙃 diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 9093e98e2e..9f1d6d3291 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -1,4 +1,9 @@ -# Typical Backstage architecture +--- +id: architecture-overview +title: Architecture overview +--- + +## Overview The following diagram shows how Backstage might look when deployed inside a company which uses the Tech Radar plugin, the Lighthouse plugin, the Circle CI @@ -14,27 +19,27 @@ Running this architecture in a real environment typically involves containerising the components. Various commands are provided for accomplishing this. -![The architecture of a basic Backstage application](./architecture-overview/backstage-typical-architecture.png) +![The architecture of a basic Backstage application](../assets/architecture-overview/backstage-typical-architecture.png) -# The UI +## The UI The UI is a thin, client-side wrapper around a set of plugins. It provides some core UI components and libraries for shared activities such as config management. [[live demo](https://backstage-demo.roadie.io/)] -![UI with different components highlighted](./architecture-overview/core-vs-plugin-components-highlighted.png) +![UI with different components highlighted](../assets/architecture-overview/core-vs-plugin-components-highlighted.png) Each plugin typically makes itself available in the UI on a dedicated URL. For example, the lighthouse plugin is registered with the UI on `/lighthouse`. [[live demo](https://backstage-demo.roadie.io/lighthouse)] -![The lighthouse plugin UI](./architecture-overview/lighthouse-plugin.png) +![The lighthouse plugin UI](../assets/architecture-overview/lighthouse-plugin.png) The Circle CI plugin is available on `/circleci`. -![Circle CI Plugin UI](./architecture-overview/circle-ci.png) +![Circle CI Plugin UI](../assets/architecture-overview/circle-ci.png) -# Plugins and plugin backends +## Plugins and plugin backends Each plugin is a client side application which mounts itself on the UI. Plugins are written in TypeScript or JavaScript. They each live in their own directory @@ -42,7 +47,7 @@ in `backstage/plugins`. For example, the source code for the lighthouse plugin is available at [backstage/plugins/lighthouse](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). -## Installing plugins +### Installing plugins Plugins are typically loaded by the UI in your Backstage applications `plugins.ts` file. For example, @@ -74,7 +79,7 @@ export default builder.build() as ApiHolder; As of this moment, there is no config based install procedure for plugins. Some code changes are required. -## Plugin architecture +### Plugin architecture Architecturally, plugins can take three forms: @@ -82,21 +87,21 @@ Architecturally, plugins can take three forms: 2. Service backed 3. Third-party backed -### Standalone plugins +#### Standalone plugins Standalone plugins run entirely in the browser. [The tech radar plugin](https://backstage-demo.roadie.io/tech-radar), for example, simply renders hard-coded information. It doesn't make any API requests to other services. -![tech radar plugin ui](./architecture-overview/tech-radar-plugin.png) +![tech radar plugin ui](../assets/architecture-overview/tech-radar-plugin.png) The architecture of the Tech Radar installed into a Backstage app is very simple. -![ui and tech radar plugin connected together](./architecture-overview/tech-radar-plugin-architecture.png) +![ui and tech radar plugin connected together](../assets/architecture-overview/tech-radar-plugin-architecture.png) -### Service backed plugins +#### Service backed plugins Service backed plugins make API requests to a service which is within the purview of the organisation running Backstage. @@ -109,7 +114,7 @@ results in a PostgreSQL database. Its architecture looks like this: -![lighthouse plugin backed to microservice and database](./architecture-overview/lighthouse-plugin-architecture.png) +![lighthouse plugin backed to microservice and database](../assets/architecture-overview/lighthouse-plugin-architecture.png) The service catalog in Backstage is another example of a service backed plugin. It retrieves a list of services, or "entities", from the Backstage Backend @@ -131,9 +136,9 @@ Cross Origin Resource Sharing policies which prevent a browser page served at [https://example.com](https://example.com) from serving resources hosted at https://circleci.com. -![CircleCi plugin talking to proxy talking to SaaS Circle CI](./architecture-overview/circle-ci-plugin-architecture.png) +![CircleCi plugin talking to proxy talking to SaaS Circle CI](../assets/architecture-overview/circle-ci-plugin-architecture.png) -# Databases +## Databases As we have seen, both the lighthouse-audit-service and catalog-backend require a database to work with. @@ -150,7 +155,7 @@ GitHub issues. [Update migrations to support postgres by dariddler · Pull Request #1527 · spotify/backstage](https://github.com/spotify/backstage/pull/1527#discussion_r450374145) -# Containerization +## Containerization The example Backstage architecture shown above would Dockerize into three separate docker images. @@ -159,7 +164,7 @@ separate docker images. 2. The backend container 3. The lighthouse audit service container -![Boxes around the architecture to indicate how it is containerised](./architecture-overview/containerised.png) +![Boxes around the architecture to indicate how it is containerised](../assets/architecture-overview/containerised.png) The frontend container can be built with a provided command. diff --git a/docs/overview/architecture-terminology.md b/docs/overview/architecture-terminology.md index 6a9d773142..aee581f927 100644 --- a/docs/overview/architecture-terminology.md +++ b/docs/overview/architecture-terminology.md @@ -1,4 +1,7 @@ -# Architecture and Terminology +--- +id: architecture-terminology +title: Architecture terminology +--- Backstage is constructed out of three parts. We separate Backstage in this way because we see three groups of contributors that work with Backstage in three diff --git a/docs/overview/background.md b/docs/overview/background.md new file mode 100644 index 0000000000..ef226653fb --- /dev/null +++ b/docs/overview/background.md @@ -0,0 +1,30 @@ +--- +id: background +title: The Spotify Story +--- + +Backstage was born out of necessity at Spotify. We found that as we grew, our +infrastructure was becoming more fragmented, our engineers less productive. + +Instead of building and testing code, teams were spending more time looking for +the right information just to get started. “Where’s the API for that service +we’re all supposed to be using?” “What version of that framework is everyone +on?” “This service isn’t responding, who owns it?” “I can’t find documentation +for anything!” + +Context switching and cognitive overload were dragging engineers down, day by +day. We needed to make it easier for our engineers to do their work without +having to become an expert in every aspect of infrastructure tooling. + +Our idea was to centralize and simplify end-to-end software development with an +abstraction layer that sits on top of all of our infrastructure and developer +tooling. That’s Backstage. + +It’s a developer portal powered by a centralized service catalog — with a plugin +architecture that makes it endlessly extensible and customizable. + +Manage all your services, software, tooling, and testing in Backstage. Start +building a new microservice using an automated template in Backstage. Create, +maintain, and find the documentation for all that software in Backstage. + +One place for everything. Accessible to everyone. diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 36ff658596..5a06f4544f 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -1,36 +1,134 @@ -# Project roadmap +--- +id: roadmap +title: Project roadmap +--- -We created Backstage about 4 years ago. While our internal version of Backstage -has had the benefit of time to mature and evolve, the first iteration of our -open source version is still nascent. We are envisioning three phases of the -project and we have already begun work on various aspects of these phases: +## Current status + +> Backstage is currently under rapid development. This means that you can expect +> APIs and features to evolve. It is also recommended that teams who adopt +> Backstage today upgrade their installation as new +> [releases](https://github.com/spotify/backstage/releases) become available, as +> Backwards compatibility is not yet guaranteed. + +## Phases + +We have divided the project into three high-level _phases_: - 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to easily create a single consistent UI layer for your internal infrastructure and tools. A set of reusable - [UX patterns and components](http://storybook.backstage.io) help ensure a + [UX patterns and components](https://backstage.io/storybook) help ensure a consistent experience between tools. - 🐢 **Phase 2:** Service Catalog ([alpha released](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)) - With a single catalog, Backstage makes it easy for a team to manage ten services — and makes it possible for your company to manage thousands of them. - Developers can get a uniform overview of all their software and related - resources, regardless of how and where they are running, as well as an easy - way to onboard and manage those resources. - 🐇 **Phase 3:** Ecosystem (later) - Everyone's infrastructure stack is different. By fostering a vibrant community of contributors we hope to provide an ecosystem of Open Source plugins/integrations that allows you to pick the tools that match your stack. -Check out our [Milestones](https://github.com/spotify/backstage/milestones) and -open [RFCs](https://github.com/spotify/backstage/labels/rfc) how they relate to -the three Phases outlined above. +## Detailed roadmap -Our vision for Backstage is for it to become the trusted standard toolbox (read: -UX layer) for the open source infrastructure landscape. Think of it like -Kubernetes for developer experience. We realize this is an ambitious goal. We -can’t do it alone. If this sounds interesting or you'd like to help us shape our -product vision, we'd love to talk. You can email me directly: +If you have questions about the roadmap or want to provide feedback, we would +love to hear from you! Please create an +[Issue](https://github.com/spotify/backstage/issues/new/choose), ping us on +[Discord](https://discord.gg/EBHEGzX) or reach out directly at [alund@spotify.com](mailto:alund@spotify.com). + +Want to help out? Awesome ❤️ Head over to +[CONTRIBUTING](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md) +guidelines to get started. + +### Ongoing work 🚧 + +- **[Plugins for managing micro services end-2-end](https://github.com/spotify/backstage/milestone/14)** - + Out of the box Backstage will ship with a set of plugins (Overview, CI, API + and Docs) that will demonstrate how a user can manage a micro service and + follow a change all the way out in production. Completing this work will make + it much easier to see how a plugin can be built that integrates with the + Backstage Service Catalog. + +- **Backstage Design System** - By providing design guidelines for common plugin + layouts together, rich set of reusable UI components + ([Storybook](https://backstage.io/storybook)) and Figma design resources. The + Design System will make it easy to design and build plugins that are + consistent across the platform -- supporting both developers and designers. + +- **[TechDocs v1](https://github.com/spotify/backstage/milestone/16)** - Our + docs-like-code feature TechDocs working end to end. + +- **[Initial GraphQL API](https://github.com/spotify/backstage/milestone/13)** - + A GraphQL API will open up the rich metadata provided by Backstage in a single + query. Plugins can easily query this API as well as extend the model where + needed. + +- **Production deployments** - Provide instructions and default configurations + (e.g. through Helm charts) for easy deployments of Backstage and its + subsystems on Kubernetes. + +- **Cloud Cost Insights plugin (from Spotify)** - Spotify teams are fully + responsible for their own software, including the cost of the cloud resources + they use. By making our internal cost insights plugin available as open source + you will also be able to treat cost as an engineering problem, and make it + easy for your engineers to see their spend and where there's opportunity to + reduce waste. + +- Further improvements to platform documentation + +### Plugins + +Building and maintaining [plugins](https://backstage.io/plugins) is the work of +the entire Backstage community. + +A list of plugins that are in development is +[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc). +We strongly recommend to upvote 👍 plugins you are interested in. This helps us +and the community prioritize what plugins to build. + +Are you missing a plugin for your favorite tool? Please +[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). +Chances are that someone will jump in and help build it. + +### Future work 🔮 + +- **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** - + The platform APIs and features are stable and can be depended on for + production use. After this plugins will require little to no maintenance. + +- **Deploy a product demo at `demo.backstage.io`** - Deploy a typical Backstage + deployment available publicly so that people can click around and get a feel + for the product without having to install anything. + +- **[Global search](https://github.com/spotify/backstage/issues/1499)** - Extend + the basic search available in the Backstage Service Catalog with a global + search experience. Long term this search solution should be extensible, making + it possible for you add custom search results. + +- **[[TechDocs V.2] Stabilization release](https://github.com/spotify/backstage/milestone/17)** - + Platform stability and compatibility improvements. + +- **Additional auth providers** - Backstage should work for most (all!) auth + solutions. Since Backstage can be used by companies regardless of what cloud + (or on prem) you are using we are especially keen to get auth support for + [AWS](https://github.com/spotify/backstage/issues/290), + [Azure](https://github.com/spotify/backstage/issues/348) and others. + +### Completed milestones ✅ + +- [Plugin marketplace](https://backstage.io/plugins) +- [Improved and move documentation to backstage.io](https://backstage.io/docs/overview/what-is-backstage) +- [Backstage Service Catalog (alpha)](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha) +- [Backstage Software Templates (alpha)](https://backstage.io/blog/2020/08/05/announcing-backstage-software-templates) +- [Make it possible to add custom auth providers](https://backstage.io/blog/2020/07/01/how-to-enable-authentication-in-backstage-using-passport) +- [TechDocs v0](https://github.com/spotify/backstage/milestone/15) +- CI plugins: CircleCI, Jenkins, GitHub Actions and TravisCI +- [Service API documentation](https://github.com/spotify/backstage/pull/1737) +- Backstage Service Catalog can read from: GitHub, GitLab, + [Bitbucket](https://github.com/spotify/backstage/pull/1938) +- Support auth providers: Google, Okta, GitHub, GitLab, + [auth0](https://github.com/spotify/backstage/pull/1611), + [AWS](https://github.com/spotify/backstage/pull/1990) diff --git a/docs/overview/support.md b/docs/overview/support.md index 7c7390fb69..9e4e9eca15 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -1,4 +1,7 @@ -# Support and community +--- +id: support +title: Support and community +--- - [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project diff --git a/docs/overview/vision.md b/docs/overview/vision.md new file mode 100644 index 0000000000..2ab7cdc6f4 --- /dev/null +++ b/docs/overview/vision.md @@ -0,0 +1,21 @@ +--- +id: vision +title: Vision +--- + +Our goal is to provide engineers with the best developer experience in the +world. + +A fantastic developer experience leads to happy, creative and productive +engineers. Our belief is that engineers should not have to be experts in various +infrastructure tools to be productive. Infrastructure should be abstracted away +so that you can return to building and scaling, quickly and safely. + +![](https://backstage.io/animations/backstage-logos-hero-8.gif) + +We are working on making Backstage the trusted standard toolbox (read: UX layer) +for the open source infrastructure landscape. Think of it like Kubernetes for +developer experience. We realize this is an ambitious goal. We can’t do it +alone. If this sounds interesting or you'd like to help us shape our product +vision, we'd love to talk. You can email me directly: +[alund@spotify.com](mailto:alund@spotify.com). diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index eb8ce1f906..96e89618a8 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -1,44 +1,53 @@ -# [Backstage](https://backstage.io) +--- +id: what-is-backstage +title: What is Backstage? +--- -![headline](../headline.png) - -## What is Backstage? +![service-catalog](https://backstage.io/blog/assets/6/header.png) [Backstage](https://backstage.io/) is an open platform for building developer -portals. It’s based on the developer portal we’ve been using internally at -Spotify for over four years. Backstage can be as simple as a services catalog or -as powerful as the UX layer for your entire tech infrastructure. +portals. Powered by a centralized service catalog, Backstage restores order to +your microservices and infrastructure. So your product teams can ship +high-quality code quickly — without compromising autonomy. -For more information go to [backstage.io](https://backstage.io) or join our -[Discord chatroom](https://discord.gg/EBHEGzX). +Backstage unifies all your infrastructure tooling, services, and documentation +to create a streamlined development environment from end to end. -### Features +Out of the box, Backstage includes: -- Create and manage all of your organization’s software and microservices in one - place. -- Services catalog keeps track of all software and its ownership. -- Visualizations provide information about your backend services and tooling, - and help you monitor them. -- A unified method for managing microservices offers both visibility and - control. -- Preset templates allow engineers to quickly create microservices in a - standardized way - ([coming soon](https://github.com/spotify/backstage/milestone/11)). -- Centralized, full-featured technical documentation with integrated tooling - that makes it easy for developers to set up, publish, and maintain alongside - their code ([coming soon](https://github.com/spotify/backstage/milestone/15)). +- [Backstage Service Catalog](../features/software-catalog/index.md) for + managing all your software (microservices, libraries, data pipelines, + websites, ML models, etc.) + +- [Backstage Software Templates](../features/software-templates/index.md) for + quickly spinning up new projects and standardizing your tooling with your + organization’s best practices + +- [Backstage TechDocs](../features/techdocs/README.md) for making it easy to + create, maintain, find, and use technical documentation, using a "docs like + code" approach + +- Plus, a growing ecosystem of + [open source plugins](https://github.com/spotify/backstage/tree/master/plugins) + that further expand Backstage’s customizability and functionality ### Benefits - For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification. + - For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation. + - For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones. + - For _everyone_, it’s a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. + +If you have questions or want support, please join our +[Discord chatroom](https://discord.gg/EBHEGzX). diff --git a/docs/plugins/add-to-marketplace.md b/docs/plugins/add-to-marketplace.md new file mode 100644 index 0000000000..d85807a226 --- /dev/null +++ b/docs/plugins/add-to-marketplace.md @@ -0,0 +1,25 @@ +--- +id: add-to-marketplace +title: Add to Marketplace +--- + +## Adding a Plugin to the Marketplace + +To add a new plugin to the [plugin marketplace](https://backstage.io/plugins) +create a file in +[`microsite/data/plugins`](https://github.com/spotify/backstage/tree/master/microsite/data/plugins) +with your plugin's information. Example: + +```yaml +--- +title: Your Plugin +author: Your Name +authorUrl: # A link to information about the author E.g. Company url, github user profile, etc +category: Monitoring # A single category e.g. CI, Machine Learning, Services, Monitoring +description: A brief description of the plugin. # Max 170 characters +documentation: # A link to your documentation E.g. Your github README +iconUrl: # Used as the src attribute for your logo. +# You can provide an external url or add your logo under static/img and provide a path +# relative to static/ e.g. img/my-logo.png +npmPackageName: # Your npm package name E.g. '@backstage/plugin-' quotes are required +``` diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index e69de29bb2..986af66c20 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -0,0 +1,6 @@ +--- +id: backend-plugin +title: Backend plugin +--- + +## TODO diff --git a/docs/plugins/call-existing-api.md b/docs/plugins/call-existing-api.md index e69de29bb2..9658176224 100644 --- a/docs/plugins/call-existing-api.md +++ b/docs/plugins/call-existing-api.md @@ -0,0 +1,172 @@ +--- +id: call-existing-api +title: Call Existing API +--- + +This article describes the various options that Backstage frontend plugins have, +in communicating with service APIs that already exist. Each section below +describes a possible choice, and the circumstances under which it fits. + +In these examples, we will be ultimately requesting data from the fictional +FrobsCo API. + +## Issuing Requests Directly + +The most basic choice available is to issue requests directly from the plugin +frontend code to the FrobsCo API, using for example `fetch` or a support library +such as `axios`. + +Example: + +```ts +// Inside your component +fetch('https://api.frobsco.com/v1/list') + .then(response => response.json()) + .then(payload => setFrobs(payload as Frob[])); +``` + +Internally at Spotify, this has not been a very common choice. Third party APIs +are sometimes accessed like this. Just a handful of internal APIs also went +through the trouble of exposing themselves in a way that is useful directly from +a browser, but even then, often not from the public internet but only supporting +users that are already on the company VPN. + +This can be used when: + +- The API already does/exposes exactly what you need. +- The request/response patterns of the API match real world usage needs in + Backstage frontend plugins. For example, if the end use case is to show a + small summary in Backstage, but the only available API endpoint gives a 30 + megabyte blob with large amounts of redundant information, it would hurt the + end user experience. Particularly on mobile. The same goes for cases where you + want to show many individual pieces of information: if a common use case is to + show large tables where one API request per cell is necessary, the browser + will quickly become swamped and you may want to consider performing + aggregation elsewhere instead. +- The API can maintain interactive request/response times at your required peak + request rates. The end user experience will be degraded if they spend a lot of + time waiting for the data to arrive. +- The API endpoint is highly available. The browser does not have builtin + facilities for load balancing, service discovery, retries, health checks, + circuit breaking and similar. If the endpoint is occasionally down even for + short periods of time (e.g. during deploys), end users will quickly notice. +- The API is exposed over HTTPS (not just HTTP), and properly handles + [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). These are + requirements that the user's browser will impose for security reasons, and the + requests will be rejected otherwise. +- The API endpoint is easily reachable, in terms of network conditions, by end + users. This may be particularly relevant if your end users are outside of your + perimeter. +- The requests do not require secrets to be passed. This limitation does not + apply to OAuth tokens, which the frontend can negotiate and make proper use + of. + +## Using The Backstage Proxy + +Backstage has an optional proxy plugin for the backend, that can be used to +easily add proxy routes to downstream APIs. + +Example: + +```yaml +# In app-config.yaml +proxy: + '/frobs': http://api.frobsco.com/v1 +``` + +```ts +// Inside your component +const backendUrl = config.getString('backend.baseUrl'); +fetch(`${backendUrl}/proxy/frobs/list`) + .then(response => response.json()) + .then(payload => setFrobs(payload as Frob[])); +``` + +The proxy is powered by the `http-proxy-middleware` package. See +[Proxying](proxying.md) for a full description of its configuration options. + +Internally at Spotify, the proxy option has been the overwhelmingly most popular +choice for plugin makers. Since we have DNS based service discovery in place and +a microservices framework that made it trivial to expose plain HTTP, it has been +a matter of just adding a few lines of Backstage config to get the benefit of +being easily and robustly reachable from users' web browsers as well. + +This may be used instead of direct requests, when: + +- You need to perform HTTPS termination and/or CORS handling, because the API + itself is not supplying those. +- You need to inject a simple static secret into the requests, e.g. an + Authorization header that gets added to the request headers. +- You want to make use of other proxy facilities, such as retries, failover, + health checks, routing, request logging, rewrites, etc. +- You already have the Backstage backend itself exposed through your perimeter + and find it practical to have only one entry point to deal with, governing + ingress with just the Backstage config. + +## Creating a Backstage Backend Plugin + +Much like the Backstage frontend, the Backstage backend also has a plugin +system. The above mentioned proxy is actually one such plugin. If you were in +need of a more involved integration than just direct access to the FrobsCo API, +or if you needed to hold state, you may want to make such a plugin. + +Example: + +```ts +// Inside your component +const backendUrl = config.getString('backend.baseUrl'); +fetch(`${backendUrl}/frobs-aggregator/summary`) + .then(response => response.json()) + .then(payload => setSummary(payload as FrobSummary)); +``` + +```ts +// Inside a new frobs-aggregator backend plugin +router.use('/summary', async (req, res) => { + const agg = await Promise.all([ + fetch('https://api.frobsco.com/v1/list'), + fetch('http://flerps.partnercompany.com:8080/flerp-batch'), + database.currentThunk(), + ]).then(async ([frobs, flerps, thunk]) => { + return computeAggregate(await frobs.json(), await flerps.json(), thunk); + }); + res.status(200).send(agg); +}); +``` + +For a more detailed example, see +[the lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse) +that stores some state in a database and adds new capabilities to the underlying +API. + +Internally at Spotify, this has been a fairly popular choice for different +reasons. Commonly, the backend has been used as a caching and data massaging +layer for slow APIs or APIs whose request/response shapes or speeds were not +acceptable for direct use by frontends. For example, this has made it possible +to issue efficient batch queries from the frontend, e.g. in big lists or tables +that want to resolve a lot of sparse data from the larger list that an +underlying service supplies. + +This may be used instead of the above, when: + +- You need to perform complex model conversion, or protocol translation beyond + what the proxy handles. +- You want to perform aggregations or summaries on the backend instead of on the + frontend. +- You want to enable batching or caching of slower or more unreliable APIs. +- You need to maintain state for your plugin, perhaps using the builtin database + support in the backend. +- You need to inject secrets or in other ways negotiate with other parts of the + API or other services in order to perform your work. +- You want to enforce end user authentication / authorization for operations on + behalf of the API, have session handling, or similar. + +There is a balance to strike regarding when to make an entirely separate backend +for a purpose, and when to make a Backstage backend plugin that adapts something +that already exists. General advice is not easy to give, but contact us on +Discord if you have any questions, and we may be able to offer guidance. + +## Extending the GraphQL Model + +The extensible GraphQL backend layer is not built yet. This section will be +expanded when that happens. Stay tuned! diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index fa91e38b06..4bcc4da387 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -1,4 +1,7 @@ -# Create a Backstage Plugin +--- +id: create-a-plugin +title: Create a Backstage Plugin +--- A Backstage Plugin adds functionality to Backstage. @@ -12,20 +15,16 @@ dependencies, then run the following on your command line (invoking the yarn create-plugin ``` -

- create plugin -

+![](../assets/getting-started/create-plugin_output.png) This will create a new Backstage Plugin based on the ID that was provided. It will be built and added to the Backstage App automatically. -_If `yarn start` is already running you should be able to see the default page -for your new plugin directly by navigating to -`http://localhost:3000/my-plugin`._ +> If `yarn start` is already running you should be able to see the default page +> for your new plugin directly by navigating to +> `http://localhost:3000/my-plugin`. -

- my plugin -

+![](../assets/my-plugin_screenshot.png) You can also serve the plugin in isolation by running `yarn start` in the plugin directory. Or by using the yarn workspace command, for example: @@ -37,7 +36,3 @@ yarn workspace @backstage/plugin-welcome start # Also supports --check This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. It is only meant for local development, and the setup for it can be found inside the plugin's `dev/` directory. - -[Next Step - Structure of a plugin](structure-of-a-plugin.md) - -[Back to Getting Started](../README.md) diff --git a/docs/plugins/existing-plugins.md b/docs/plugins/existing-plugins.md index bd6fd19691..8598ae02c4 100644 --- a/docs/plugins/existing-plugins.md +++ b/docs/plugins/existing-plugins.md @@ -1,4 +1,7 @@ -# Existing plugins +--- +id: existing-plugins +title: Existing plugins +--- ## Open source plugins diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 5021ac9896..5aaf8acadf 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -1,4 +1,7 @@ -# Plugins +--- +id: index +title: Intro to plugins +--- Backstage is a single-page application composed of a set of plugins. @@ -8,7 +11,7 @@ development tool as a plugin in Backstage. By following strong [design guidelines](../dls/design.md) we ensure the the overall user experience stays consistent between plugins. -![plugin](my-plugin_screenshot.png) +![plugin](../assets/my-plugin_screenshot.png) ## Creating a plugin @@ -18,8 +21,15 @@ To create a plugin, follow the steps outlined [here](create-a-plugin.md). If you start developing a plugin that you aim to release as open source, we suggest that you create a new -[new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md). +[new Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development. You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work. + +## Integrate into the Service Catalog + +If your plugin isn't supposed to live as a standalone page, but rather needs to +be presented as a part of a Service Catalog (e.g. a separate tab or a card on an +"Overview" tab), then check out +[the instruction](integrating-plugin-into-service-catalog.md). on how to do it. diff --git a/docs/plugins/integrating-plugin-into-service-catalog.md b/docs/plugins/integrating-plugin-into-service-catalog.md new file mode 100644 index 0000000000..4408c27ccc --- /dev/null +++ b/docs/plugins/integrating-plugin-into-service-catalog.md @@ -0,0 +1,124 @@ +--- +id: integrating-plugin-into-service-catalog +title: Integrate into the Service Catalog +--- + +> This is an advanced use case and currently is an experimental feature. Expect +> API to change over time + +## Steps + +1. [Create a plugin](#create-a-plugin) +1. [Export a router with relative routes](#export-a-router) +1. [Import and use router in the APP](#import-and-use-router-in-the-app) + +### Create a plugin + +Follow the [same process](create-a-plugin.md) as for standalone plugin. You +should have a separate package in a folder, which represents your plugin. + +Example: + +``` +$ yarn create-plugin +> ? Enter an ID for the plugin [required] my-plugin +> ? Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional] + +Creating the plugin... +``` + +### Export a router + +Now in the plugin you have a `Router.tsx` file in the `src` folder. By default +it contains only one example route. Create a routing structure needed for your +plugin, keeping in mind that the whole set of routes defined here are going to +be mounted under some different route in the App. + +Example: + +`my-plugin` consists of 2 different views - `/me` and `/about`. I envision +people integrating it into plugin catalog as a tab named "MyPlugin". Then, my +`Routes.tsx` for the plugin is going to look like: + +```tsx + + } /> + } /> + +``` + +(where MePage and AboutPage are 2 components defined in your plugin and imported +accordingly inside `Router.tsx`) + +> Pay attention, if your `MePage` references the `AboutPage` it needs to do it +> through link to `about`, not `/about`. This allows react-router v6 to enable +> its relative routing mechanism. Read more - +> https://reacttraining.com/blog/react-router-v6-pre/#relative-route-path-and-link-to + +### Import and use router in the APP + +In the `app/src/components/catalog/EntityPage.tsx` (app === your folder, +containing backstage app) import your created Router: + +```tsx +import { Router as MyPluginRouter } from '@backstage/plugin-my-plugin; +``` + +Now, you need to mount `MyPluginRouter` onto some route, for example if you had: + +```tsx +const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); +``` + +after you add your code it becomes: + +```tsx +const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + } + /> + +); +``` + +All of magic happens thanks to the `EntityPageLayout` component, which comes as +an export from `@backstage/plugin-catalog` package. + +```tsx +type EntityPageLayoutContentProps = { + /** + * Going to be transformed into react-router v6 + * path under the hood. Read more at https://reacttraining.com/blog/react-router-v6-pre + */ + path: string; + /** + * Gets transformed into the title for the tab + */ + title: string; + /** + * Element that is rendered when the location + * matches the path provided + */ + element: JSX.Element; +}; +``` + +> You can either pass the entity from App to the plugin's router as a prop or +> use `useEntity` hook from `@backstage/plugin-catalog` directly inside your +> plugin. diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index 41918b5de2..faf3c6ddbd 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -1,4 +1,7 @@ -# Plugin Development in Backstage +--- +id: plugin-development +title: Plugin Development +--- Backstage plugins provide features to a Backstage App. @@ -7,28 +10,12 @@ type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data from external sources using the regular browser APIs or by depending on external modules to do the work. - - ## Developing guidelines - Consider writing plugins in `TypeScript`. - Plan the directory structure of your plugin so that it becomes easy to manage. -- Prefer using the Backstage components, otherwise go with - [Material-UI](https://material-ui.com/). +- Prefer using the [Backstage components](https://backstage.io/storybook), + otherwise go with [Material-UI](https://material-ui.com/). - Check out the shared Backstage APIs before building a new one. ## Plugin concepts / API diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index e69de29bb2..658df3dda1 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -0,0 +1,75 @@ +--- +id: proxying +title: Proxying +--- + +## Overview + +The Backstage backend comes packaged with a basic HTTP proxy, that can aid in +reaching backend service APIs from frontend plugin code. See +[Call Existing API](call-existing-api.md) for a description of when the proxy +can be the best choice for communicating with an API. + +## Getting Started + +The plugin is already added to a default Backstage project. + +In `packages/backend/src/index.ts`: + +```ts +const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + +const service = createServiceBuilder(module) + .loadConfig(configReader) + /** ... other routers ... */ + .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); +``` + +## Configuration + +Configuration for the proxy plugin lives under a `proxy` root key of your +`app-config.yaml` file. + +Example: + +```yaml +# in app-config.yaml +proxy: + '/simple-example': http://simple.example.com:8080 + '/larger-example/v1': + target: http://larger.example.com:8080/svc.v1 + headers: + Authorization: + $secret: + env: EXAMPLE_AUTH_HEADER +``` + +Each key under the proxy configuration entry is a route to match, below the +prefix that the proxy plugin is mounted on. It must start with a slash. For +example, if the backend mounts the proxy plugin as `/proxy`, the above +configuration will lead to the proxy acting on backend requests to +`/proxy/simple-example/...` and `/proxy/larger-example/v1/...`. + +The value inside each route is either a simple URL string, or an object on the +format accepted by +[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). + +If the value is a string, it is assumed to correspond to: + +```yaml +target: +changeOrigin: true +pathRewrite: + '^/': '/' +``` + +When the target is an object, it is given verbatim to `http-proxy-middleware` +except with the following caveats for convenience: + +- If `changeOrigin` is not specified, it is set to `true`. This is the most + commonly useful value. +- If `pathRewrite` is not specified, it is set to a single rewrite that removes + the entire prefix and route. In the above example, a rewrite of + `'^/proxy/larger-example/v1/': '/'` is added. That means that a request to + `/proxy/larger-example/v1/some/path` will be translated to a request to + `http://larger.example.com:8080/svc.v1/some/path`. diff --git a/docs/plugins/publish-private.md b/docs/plugins/publish-private.md index e69de29bb2..6361f70854 100644 --- a/docs/plugins/publish-private.md +++ b/docs/plugins/publish-private.md @@ -0,0 +1,6 @@ +--- +id: publish-private +title: Publish private +--- + +## TODO diff --git a/docs/plugins/publishing.md b/docs/plugins/publishing.md index 76997c30d2..6fee4f9b1d 100644 --- a/docs/plugins/publishing.md +++ b/docs/plugins/publishing.md @@ -1,4 +1,7 @@ -# Publishing +--- +id: publishing +title: Publishing +--- ## NPM @@ -36,4 +39,18 @@ $ git push origin -u new-release And then create a PR. Once the PR is approved and merged into master, the master build will publish new versions of all bumped packages. +### Include new changes in existing release PR + +If you want to include some last minute changes to an existing release PR, +follow these instructions: + +```sh +$ git checkout master +$ git pull +$ git checkout new-release +$ git reset --hard master +$ yarn release +$ git push --force +``` + [Back to Docs](../README.md) diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index ceacf578fe..20e72c539d 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -1,4 +1,7 @@ -# Structure of a Plugin +--- +id: structure-of-a-plugin +title: Structure of a Plugin +--- Nice, you have a new plugin! We'll soon see how we can develop it into doing great things. But first off, let's look at what we get out of the box. diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index 701db3eff4..110bc97515 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -1,4 +1,7 @@ -# Testing with Jest +--- +id: testing +title: Testing with Jest +--- Backstage uses [Jest](https://facebook.github.io/jest/) for all our unit testing needs. diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index 0f1debd515..bc38656939 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -1,4 +1,7 @@ -# createPlugin - feature flags +--- +id: createPlugin-feature-flags +title: createPlugin - feature flags +--- The `featureFlags` object passed to the `register` function makes it possible for plugins to register Feature Flags in Backstage for users to opt into. You diff --git a/docs/reference/createPlugin-router.md b/docs/reference/createPlugin-router.md index 831b27f388..361b485df9 100644 --- a/docs/reference/createPlugin-router.md +++ b/docs/reference/createPlugin-router.md @@ -1,4 +1,7 @@ -# createPlugin - router +--- +id: createPlugin-router +title: createPlugin - router +--- The router that is passed to the `register` function makes it possible for plugins to hook into routing of the Backstage app and provide the end users with @@ -34,5 +37,3 @@ const myPluginRouteRef = createRouteRef({ title: 'My Plugin', }); ``` - -[Back to References](../README.md) diff --git a/docs/reference/createPlugin.md b/docs/reference/createPlugin.md index 7ecb6d461a..0cabe63b39 100644 --- a/docs/reference/createPlugin.md +++ b/docs/reference/createPlugin.md @@ -1,4 +1,7 @@ -# createPlugin +--- +id: createPlugin +title: createPlugin +--- Taking a plugin config as argument and returns a new plugin. diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md index 2c57199fba..eda1bafef0 100644 --- a/docs/reference/utility-apis/AppThemeApi.md +++ b/docs/reference/utility-apis/AppThemeApi.md @@ -178,7 +178,10 @@ type PaletteAdditions = { linkHover: string; link: string; gold: string; - sidebar: string; + navigation: { + background: string; + indicator: string; + }; tabbar: { indicator: string; }; diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md index 8b9ebcbdad..521628fd33 100644 --- a/docs/reference/utility-apis/README.md +++ b/docs/reference/utility-apis/README.md @@ -1,11 +1,14 @@ -# Backstage Core Utility APIs +--- +id: README +title: Utility API References +--- The following is a list of all Utility APIs defined by `@backstage/core`. They are available to use by plugins and components, and can be accessed using the `useApi` hook, also provided by `@backstage/core`. For more information, see https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md. -### alert +## alert Used to report alerts and forward them to the app @@ -14,7 +17,7 @@ Implemented type: [AlertApi](./AlertApi.md) ApiRef: [alertApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AlertApi.ts#L41) -### appTheme +## appTheme API Used to configure the app theme, and enumerate options @@ -23,7 +26,7 @@ Implemented type: [AppThemeApi](./AppThemeApi.md) ApiRef: [appThemeApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74) -### config +## config Used to access runtime configuration @@ -32,7 +35,7 @@ Implemented type: [Config](./Config.md) ApiRef: [configApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) -### error +## error Used to report errors and forward them to the app @@ -41,7 +44,7 @@ Implemented type: [ErrorApi](./ErrorApi.md) ApiRef: [errorApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) -### featureFlags +## featureFlags Used to toggle functionality in features across Backstage @@ -50,9 +53,9 @@ Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md) ApiRef: [featureFlagsApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58) -### githubAuth +## githubAuth -Provides authentication towards Github APIs +Provides authentication towards GitHub APIs Implemented types: [OAuthApi](./OAuthApi.md), [ProfileInfoApi](./ProfileInfoApi.md), @@ -62,9 +65,9 @@ Implemented types: [OAuthApi](./OAuthApi.md), ApiRef: [githubAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L230) -### gitlabAuth +## gitlabAuth -Provides authentication towards Gitlab APIs +Provides authentication towards GitLab APIs Implemented types: [OAuthApi](./OAuthApi.md), [ProfileInfoApi](./ProfileInfoApi.md), @@ -74,7 +77,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), ApiRef: [gitlabAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L260) -### googleAuth +## googleAuth Provides authentication towards Google APIs and identities @@ -87,7 +90,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), ApiRef: [googleAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L213) -### identity +## identity Provides access to the identity of the signed in user @@ -96,7 +99,7 @@ Implemented type: [IdentityApi](./IdentityApi.md) ApiRef: [identityApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) -### oauth2 +## oauth2 Example of how to use oauth2 custom provider @@ -107,7 +110,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), ApiRef: [oauth2ApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L270) -### oauthRequest +## oauthRequest An API for implementing unified OAuth flows in Backstage @@ -116,7 +119,7 @@ Implemented type: [OAuthRequestApi](./OAuthRequestApi.md) ApiRef: [oauthRequestApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) -### oktaAuth +## oktaAuth Provides authentication towards Okta APIs @@ -129,7 +132,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), ApiRef: [oktaAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L243) -### storage +## storage Provides the ability to store data which is unique to the user diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/journey.md b/docs/tutorials/journey.md similarity index 94% rename from docs/journey.md rename to docs/tutorials/journey.md index e38130afb7..3c399c53ba 100644 --- a/docs/journey.md +++ b/docs/tutorials/journey.md @@ -1,12 +1,15 @@ -# Purpose +--- +id: journey +title: Future developer journey +--- -This RFC describes a possible journey of a future Backstage plugin developer as -they build a plugin that touches many different aspects of a Backstage. The -story invents many new things that are not part of Backstage today, but are -things that I'm suggesting we should add as long term or north star goals. The -idea is to discuss what parts of the story makes sense to aim for, and what we'd -want to do differently or not at all. The "chapters" are numbered to make it a -bit easier to comment on parts of the story. +> This document describes a possible journey of a **_future_** Backstage plugin +> developer as they build a plugin that touches many different aspects of a +> Backstage. The story invents many new things that are not part of Backstage +> today, but are things that I'm suggesting we should add as long term or north +> star goals. The idea is to discuss what parts of the story makes sense to aim +> for, and what we'd want to do differently or not at all. The "chapters" are +> numbered to make it a bit easier to comment on parts of the story. # The Protagonist diff --git a/docs/verify-links.js b/docs/verify-links.js new file mode 100755 index 0000000000..a218cde71b --- /dev/null +++ b/docs/verify-links.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { resolve: resolvePath, dirname } = require('path'); +const fs = require('fs-extra'); +const recursive = require('recursive-readdir'); + +const projectRoot = resolvePath(__dirname, '..'); + +async function verifyUrl(basePath, url) { + // Avoid having absolute URL links within docs/, so that links work on the site + if ( + url.match( + /https:\/\/github.com\/spotify\/backstage\/(tree|blob)\/master\/docs\//, + ) && + basePath.match(/^(?:docs|microsite)\//) + ) { + return { url, basePath, problem: 'absolute' }; + } + + url = url.replace(/#.*$/, ''); + url = url.replace( + /https:\/\/github.com\/spotify\/backstage\/(tree|blob)\/master/, + '', + ); + if (!url) { + return; + } + + // Only verify existence of local files for now, so skip anything with a schema + if (url.match(/[a-z]+:/)) { + return; + } + + let path = ''; + + if (url.startsWith('/')) { + if (url.startsWith('/docs/') && basePath.match(/^(?:docs|microsite)\//)) { + return { url, basePath, problem: 'not-relative' }; + } + + path = resolvePath(projectRoot, `.${url}`); + } else { + path = resolvePath(dirname(resolvePath(projectRoot, basePath)), url); + } + + const exists = await fs.pathExists(path); + if (!exists) { + return { url, basePath, problem: 'missing' }; + } + + return; +} + +async function verifyFile(filePath) { + const content = await fs.readFile(filePath, 'utf8'); + const mdLinks = content.match(/\[.+?\]\(.+?\)/g) || []; + const badUrls = []; + + for (const mdLink of mdLinks) { + const url = mdLink.match(/\[.+\]\((.+)\)/)[1].trim(); + const badUrl = await verifyUrl(filePath, url); + if (badUrl) { + badUrls.push(badUrl); + } + } + + return badUrls; +} + +async function main() { + process.chdir(projectRoot); + + const files = await recursive('.', [ + 'node_modules', + 'dist', + 'bin', + 'microsite', + ]); + const mdFiles = files.filter(f => f.endsWith('.md')); + const badUrls = []; + + for (const mdFile of mdFiles) { + const badFileUrls = await verifyFile(mdFile); + badUrls.push(...badFileUrls); + } + + if (badUrls.length) { + console.log(`Found ${badUrls.length} bad links within repo`); + for (const { url, basePath, problem } of badUrls) { + if (problem === 'missing') { + console.error(`Unable to reach ${url}, linked from ${basePath}`); + } else if (problem === 'not-relative') { + console.error('Links to /docs/ must be relative'); + console.error(` From: ${basePath}`); + console.error(` To: ${url}`); + } else if (problem === 'absolute') { + console.error(`Link to docs/ should be replaced by a relative URL`); + console.error(` From: ${basePath}`); + console.error(` To: ${url}`); + } + } + process.exit(1); + } +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/install/kubernetes/backstage/README.md b/install/kubernetes/backstage/README.md index e8d809c228..fc3d20733b 100644 --- a/install/kubernetes/backstage/README.md +++ b/install/kubernetes/backstage/README.md @@ -23,7 +23,7 @@ | app.resources | Kubernetes Pod resource requests/limits | `{}` | | app.nodeSelector | Node selectors for scheduling app/frontend pods | `{}` | | app.tolerations | Tolerations for scheduling app/frontend pods | `{}` | -| app.affinity | Affinity setttings for scheduling app/frontend pods | `{}` | +| app.affinity | Affinity settings for scheduling app/frontend pods | `{}` | ## Backend Values @@ -48,4 +48,4 @@ | backend.resources | Kubernetes Pod resource requests/limits | `{}` | | backend.nodeSelector | Node selectors for scheduling backend pods | `{}` | | backend.tolerations | Tolerations for scheduling backend pods | `{}` | -| backend.affinity | Affinity setttings for scheduling backend pods | `{}` | +| backend.affinity | Affinity settings for scheduling backend pods | `{}` | diff --git a/lerna.json b/lerna.json index 35f84c54d0..63e4701ce3 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.18" + "version": "0.1.1-alpha.20" } diff --git a/microsite/README.md b/microsite/README.md new file mode 100644 index 0000000000..d7187888a3 --- /dev/null +++ b/microsite/README.md @@ -0,0 +1,182 @@ +This website was created with [Docusaurus](https://docusaurus.io/). + +# What's In This Document + +- [Get Started in 5 Minutes](#get-started-in-5-minutes) +- [Directory Structure](#directory-structure) +- [Editing Content](#editing-content) +- [Adding Content](#adding-content) +- [Full Documentation](#full-documentation) + +## Directory Structure + +Your project file structure should look something like this + +``` +my-docusaurus/ + docs/ + doc-1.md + doc-2.md + doc-3.md + website/ + blog/ + 2016-3-11-oldest-post.md + 2017-10-24-newest-post.md + core/ + node_modules/ + pages/ + static/ + css/ + img/ + package.json + sidebars.json + siteConfig.js +``` + +# Editing Content + +## Editing an existing docs page + +Edit docs by navigating to `docs/` and editing the corresponding document: + +`docs/doc-to-be-edited.md` + +```markdown +--- +id: page-needs-edit +title: This Doc Needs To Be Edited +--- + +Edit me... +``` + +For more information about docs, click [here](https://docusaurus.io/docs/en/navigation) + +## Editing an existing blog post + +Edit blog posts by navigating to `website/blog` and editing the corresponding post: + +`website/blog/post-to-be-edited.md` + +```markdown +--- +id: post-needs-edit +title: This Blog Post Needs To Be Edited +--- + +Edit me... +``` + +For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog) + +# Adding Content + +## Adding a new docs page to an existing sidebar + +1. Create the doc as a new markdown file in `/docs`, example `docs/newly-created-doc.md`: + +```md +--- +id: newly-created-doc +title: This Doc Needs To Be Edited +--- + +My new content here.. +``` + +1. Refer to that doc's ID in an existing sidebar in `website/sidebars.json`: + +```javascript +// Add newly-created-doc to the Getting Started category of docs +{ + "docs": { + "Getting Started": [ + "quick-start", + "newly-created-doc" // new doc here + ], + ... + }, + ... +} +``` + +For more information about adding new docs, click [here](https://docusaurus.io/docs/en/navigation) + +## Adding a new blog post + +1. Make sure there is a header link to your blog in `website/siteConfig.js`: + +`website/siteConfig.js` + +```javascript +headerLinks: [ + ... + { blog: true, label: 'Blog' }, + ... +] +``` + +2. Create the blog post with the format `YYYY-MM-DD-My-Blog-Post-Title.md` in `website/blog`: + +`website/blog/2018-05-21-New-Blog-Post.md` + +```markdown +--- +author: Frank Li +authorURL: https://twitter.com/foobarbaz +authorFBID: 503283835 +title: New Blog Post +--- + +Lorem Ipsum... +``` + +For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog) + +## Adding items to your site's top navigation bar + +1. Add links to docs, custom pages or external links by editing the headerLinks field of `website/siteConfig.js`: + +`website/siteConfig.js` + +```javascript +{ + headerLinks: [ + ... + /* you can add docs */ + { doc: 'my-examples', label: 'Examples' }, + /* you can add custom pages */ + { page: 'help', label: 'Help' }, + /* you can add external links */ + { href: 'https://github.com/facebook/docusaurus', label: 'GitHub' }, + ... + ], + ... +} +``` + +For more information about the navigation bar, click [here](https://docusaurus.io/docs/en/navigation) + +## Adding custom pages + +1. Docusaurus uses React components to build pages. The components are saved as .js files in `website/pages/en`: +1. If you want your page to show up in your navigation header, you will need to update `website/siteConfig.js` to add to the `headerLinks` element: + +`website/siteConfig.js` + +```javascript +{ + headerLinks: [ + ... + { page: 'my-new-custom-page', label: 'My New Custom Page' }, + ... + ], + ... +} +``` + +For more information about custom pages, click [here](https://docusaurus.io/docs/en/custom-pages). + +# Full Documentation + +Full documentation can be found on the [website](https://docusaurus.io/). diff --git a/microsite/blog/2020-03-16-announcing-backstage.md b/microsite/blog/2020-03-16-announcing-backstage.md new file mode 100644 index 0000000000..3911debf08 --- /dev/null +++ b/microsite/blog/2020-03-16-announcing-backstage.md @@ -0,0 +1,44 @@ +--- +title: Announcing Backstage +author: Stefan Ålund +authorURL: http://twitter.com/stalund +authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg +--- + +## What is Backstage? + +Backstage is Spotify's open source platform for building developer portals. + +It’s the first open source infrastructure platform by Spotify that allows you to focus on building your application instead of reinventing the button. With an elegant and unified, yet opinionated UI/UX for all your tooling and infrastructure, Backstage enables engineers to get up and running faster, which ultimately makes their lives easier and more productive. + +![img](assets/blog_1.png) + + + +## As simple as writing a plugin. + +Backstage makes it easy to unify all of your infrastructure tooling, services, and documentation under a single, easy-to-use interface. So your engineers will always know where to find the right tool for the job. And engineers will already know how to use each tool — because everything uses the same, familiar UI. + +The number of open source infrastructure projects and tools [landscape](https://landscape.cncf.io/) is exploding. As the sheer volume of projects increases, companies and their engineers find it increasingly difficult to keep track and adopt all of the tooling fast enough to keep pace. Most of the tools were built by a different individual, team, or company, which means that there is no single UI/UX, and simply getting the tool installed and started can be a painful challenge- let alone wrangling each tool to work with one another within your existing ecosystem. Due to varying qualities and the varying UI/UX of each open source project, we'd like to introduce Backstage as a best-of-breed platform for developers to use... all in service of ensuring a flawless, consistent user experience. + +![illustration](assets/illustration.svg) + +## The Spotify story + +A best-in-class developer portal — from a music company? Since the very beginning, Spotify has been known for its agile, autonomous engineering culture. More than music, we’re a tech company that has always put engineers first, empowering our developers with the ability to innovate quickly and at scale. Backstage is the natural result of that focus. + +Since adopting Backstage internally at Spotify, we’ve seen a 55% decrease in onboarding time for our engineers (as measured by time until 10th pull request). Over 280 engineering teams inside Spotify are using Backstage to manage 2,000+ backend services, 300+ websites, 4,000+ data pipelines, and 200+ mobile features. + +## Project roadmap + +We created Backstage about 4 years ago, and today, we’ve decided to share the goodness with the greater engineering community. While our version of Backstage has had the benefit of time to mature and evolve, the first iteration of our open source version is still nascent. I wanted to take a moment to share with you what our vision for Backstage OSS is so that 1. users and our community gain a better understanding of where we’re envisioning the product to go and more importantly, 2. you can provide input and feedback so that together, we can create a better infrastructure experience for developers everywhere. + +We are envisioning three phases of the project and we have already begun work on various aspects of these phases: + +- **Phase 1:** Extensible frontend platform (now) - You will be able to easily create a single consistent UI layer for your internal infrastructure and tools. A set of reusable UX patterns and components help ensure a consistent experience between tools. + +- **Phase 2:** Manage your stuff (next 2-3 months) - Manage anything from microservices to software components to infrastructure and your service catalog. Regardless of whether you want to create a new library, view service deployment status in Kubernetes, or check the test coverage for a website -- Backstage will provide all of those tools - and many more - in a single developer portal. + +- **Phase 3:** Ecosystem (later) - Everyone's infrastructure stack is different. By fostering a vibrant community of contributors we hope to provide an ecosystem of Open Source plugins/integrations that allows you to pick the tools that match your stack. + +Our vision for Backstage is for it to become the trusted standard toolbox (read: UI layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. If this sounds interesting or you'd like to help us shape our product vision, we'd love to talk. You can email me directly: [alund@spotify.com](mailto:alund@spotify.com). diff --git a/microsite/blog/2020-03-18-what-is-backstage.md b/microsite/blog/2020-03-18-what-is-backstage.md new file mode 100644 index 0000000000..8b0a0fcbe6 --- /dev/null +++ b/microsite/blog/2020-03-18-what-is-backstage.md @@ -0,0 +1,88 @@ +--- +title: What the heck is Backstage anyway? +author: Stefan Ålund +authorURL: http://twitter.com/stalund +authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg +--- + +![img](assets/2/spotify-labs-header.png) + +Two days ago, we released the open source version of [Backstage](https://backstage.io/), our homegrown developer portal. And we learned a thing or two via the feedback we received. So, I wanted to take this opportunity to further explain what we’re trying to do with Backstage — and more importantly, what we want to give to the greater engineering community beyond Spotify. + + + +## What’s the big infrastructure problem? + +As companies grow, their infrastructure systems get messier. Consider a team that wants to deploy something to the cloud. While Spotify has many awesome engineers, not every engineer is well-versed in our chosen cloud-provider tooling. Yet everyone is required to know and understand Terraform, GCP/AWS/Azure CLIs, GitLab CI, Prometheus, Kubernetes, Docker, various monitoring and alerting tools, and much, much more. Once other resources come into play (databases, queueing, etc.), each engineer requires even more tools and domain-specific knowledge (or “disciplines”), from backend to machine learning, to mobile and data. + +## What’s the fix? + +Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI. All of it! Imagine if all your tools — GCP, Bigtable, CI pipelines, TensorFlow Extended, and whatever else is hiding in your stack — all had the same, easy-to-use interface. That’s Backstage. One front end for all your infrastructure. + +![img](assets/2/screen.gif) + +Backstage gives developers a uniform overview of all their resources, regardless of how and where they are running, as well as an easy way to onboard and start using those tools. It also allows the creation of new resources, such as backend services running in Kubernetes, with a few clicks of a button — all without having to leave the same, familiar interface of Backstage. + +## Why did we build it? + +To some observers, it may seem odd that a music company is launching a best-in-class developer portal. But if you [dig deeper](https://backstage.io/background), you’ll find that since the very beginning, Spotify has been known for its agile, autonomous engineering culture. More than music, we’re a tech company that has always put engineers first, empowering our developers with the ability to innovate quickly and at scale. Backstage is the natural result of that focus. + +## What are examples of how Backstage is used at Spotify? + +Our internal installation of Backstage has over 100 different integrations — we call them “plugins”. Since the open-source version currently does not have any end-to-end use cases, it can be challenging to understand what problems Backstage can solve for you. To make things more tangible, let’s have a look at four of the common use-cases: + +1. Creating a new microservice +2. Following a pull request from review to production +3. Centralised technical documentation +4. Review performance of your team’s mobile features + +These are just a few examples. Expect us to continue providing examples of how Backstage is used inside Spotify while we build out more end-2-end use-cases in the open. + +### 1. Creating a new microservice + +Creating any new software component at Spotify, such as a new microservice, is done with a few clicks in Backstage. Developers choose between a number of standard templates — all with best-practices built in. + +![img](assets/2/1.png) + +After inputting some metadata about your service, a new repository is created with a “hello world” service that automatically builds and deploys in production on Kubernetes ([GKE](https://cloud.google.com/kubernetes-engine)). Ownership information is automatically captured in our service/software catalog and users can see a list of all the services they own. + +![img](assets/2/2.png) + +### 2. Following a pull request from review to production + +As soon as you submit a pull request to Spotify’s GitHub Enterprise, our CI system automatically posts a link to the CI/CD view in Backstage. The view provides you with all the information you need: build progress, test coverage changes, a re-trigger button, etc., so that you don’t have to look for this information across different systems. + +![img](assets/2/3.png) + +Our homegrown CI system uses Jenkins under the hood, but Spotify engineers don’t need to know that. They interact directly with GitHub Enterprise and Backstage. + +### 3. Centralised technical documentation + +Spotify uses a [docs-like-code](https://www.youtube.com/watch?v=uFGCaZmA6d4) approach. Engineers write technical documentation in Markdown files that live together with the code. During CI, a beautiful-looking documentation site is created using [MkDocs](https://www.mkdocs.org/), and all sites are rendered centrally in a Backstage plugin. + +![img](assets/2/4.png) + +On top of the static documentation we also incorporate additional metadata about the documentation site — such as owner, open issue and related Stack Overflow tags. + +### 4. Review performance of your team’s mobile features + +Our mobile apps are developed by many different teams. The codebase is divided up into different features, each owned and maintained by a separate team. If an app developer on one team wants to understand how their feature is affecting overall app performance, there’s a plugin for that: + +![img](assets/2/5.png) +_Figures above for illustrative purposes only._ + +Developers can also look at crashes, releases, test coverage over time and many more tools in the same location. + +## Why did we make Backstage open source? + +When discussing infrastructure challenges with peer companies, it’s clear that we are not alone in struggling with fragmentation across our developer ecosystem. As companies adopt more open-source tooling, and build more infrastructure internally, the complexity grows. It gets harder for individual engineers to find and use all these distinct tools. + +Similar to how Backstage ties together all of Spotify’s infrastructure, our ambition is to make the open-source version of Backstage the standard UX layer across the broader infrastructure landscape. We decided to release Backstage early so we could collaborate more closely with companies that have a similar problem — and that want to provide a better developer experience to their teams. + +## What’s next? + +We are envisioning [three phases](https://github.com/spotify/backstage/milestones) of the project (so far), and we have already begun work on various aspects of these phases. The best way to track the work and see where you can jump in and help out is: + +https://github.com/spotify/backstage/milestones + +Want to discuss the project or need support? Join us on [Discord](https://discord.gg/MUpMjP2) or reach out on [alund@spotify.com](mailto:alund@spotify.com). diff --git a/microsite/blog/2020-04-06-lighthouse-plugin.md b/microsite/blog/2020-04-06-lighthouse-plugin.md new file mode 100644 index 0000000000..2d5c7b96b7 --- /dev/null +++ b/microsite/blog/2020-04-06-lighthouse-plugin.md @@ -0,0 +1,40 @@ +--- +title: Introducing Lighthouse for Backstage +author: Paul Marbach +authorURL: http://twitter.com/fastfrwrd +authorImageURL: https://pbs.twimg.com/profile_images/1224058798958088192/JPxS8uzR_400x400.jpg +--- + +![image illustrating the Lighthouse plugin for Backstage](assets/3/lead.png) + +We’re proud to announce that our first internal plugin at Spotify has been open-sourced as part of Backstage. This plugin works with the newly open-sourced [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service) to run and track Lighthouse audits for your websites. + + + +## What is Lighthouse? + +Google's [Lighthouse](https://developers.google.com/web/tools/lighthouse) auditing tool for websites is a great open-source resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your site. + +At Spotify, we keep track of Lighthouse audit scores over time to look at trends and areas for investment. We particularly look to Lighthouse to give us [accessibility recommendations](https://developers.google.com/web/tools/lighthouse/v3/scoring#a11y); in the next few months, we plan to roll out Lighthouse accessibility category scores as a benchmark metric for all websites at Spotify. + +## Lighthouse, tracked over time + +What makes the plugin unique is that we can track a website's audit performance over time using the main metrics that Lighthouse outputs, rather than simply running reports. The sparklines show, at a glance, how all of your websites are holding up over recent builds. + +![image of the audit list in the Lighthouse plugin](assets/3/audit-list.png) + +Lighthouse reports can be viewed directly in Backstage, with the ability to travel back and forth through your audit history, so you can quickly diagnose which release caused a performance or SEO regression. + +![image of the audit view in the Lighthouse plugin](assets/3/audit-view.png) + +Trigger an audit directly from Backstage, or trigger audits programmatically with your new lighthouse-audit-service instance. Schedule them after builds as a sort of smoke test, or trigger them on a schedule (as we do at Spotify) to get a daily snapshot of your website. + +![image of the create audit form in the Lighthouse plugin](assets/3/create-audit.png) + +## Using Lighthouse in Backstage + +To learn how you can enable Lighthouse auditing within Backstage, head over to the [README](https://github.com/spotify/backstage/tree/master/plugins/lighthouse) for the plugin to get started. + +## A personal note + +I want to thank the folks on the Backstage team for approaching me to open-source this plugin. I have found working on Backstage to be a really rewarding and fun time, and I'm so glad that the core team members have put in the effort to make Backstage something that anyone in the industry can use. I can't wait to play with all the plugins the community is going to create. I am hopeful that this plugin can help illustrate just a sliver of what we use Backstage for at Spotify. diff --git a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md new file mode 100644 index 0000000000..65ee02f6a0 --- /dev/null +++ b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md @@ -0,0 +1,135 @@ +--- +title: How to quickly set up Backstage +author: Marcus Eide +authorURL: https://github.com/marcuseide +authorImageURL: https://secure.gravatar.com/avatar/20223f1e03673c7c1e6282fbebaf6942 +--- + +We wanted to make getting started with Backstage as easy as possible. Even though Backstage is still in the early phases of its development, we believe it is important for our users to get a feel for what Backstage really is. + +We want users to be able to create their own version of Backstage quickly and easily, so that they can take advantage of all the infrastructure that we’ve built into it — and start exploring. + +In this blog post we’ll look at what a Backstage app is and how to create one using our [CLI](https://www.npmjs.com/package/@backstage/cli). + + + +## What is a Backstage app? + +![](assets/4/welcome.png) + +A Backstage app is a modern monorepo web project that is built using Backstage packages. It includes all the configuration and architecture you need to run Backstage so that you don’t have to worry about setting everything up by yourself. + +More specifically, a Backstage app includes the core packages and APIs that provide base functionality to the app. The actual UX is provided by plugins. As an example, when you first load the `/` page of the app, the content is provided by the `welcome` plugin. + +Plugins are the essential building blocks of Backstage and extend the platform by providing additional features and functionality. Read more about [Backstage plugins](/docs/getting-started) on GitHub. + +## A personalized platform + +When you create a Backstage _app_, you are creating your own installation of Backstage, an application that is built on top of the Backstage _platform_. + +You get to take full advantage of a platform that we at Spotify have been using internally for years. But you also get to make it your own — starting with its name. You can rename the Backstage app anything you want, so that you can call it something that best fits your organization. Be creative! + +## How do I create an app? + +Just run the backstage-cli: + +```bash +npx @backstage/cli create-app +``` + +Name your app, and we will create everything you need: + +![](assets/4/create-app.png) + +The only thing you need to do is to start the app: + +```bash +cd my-app +yarn start +``` + +And you are good to go! 👍 + +Read the full documentation on how to [create an app](/docs/getting-started/create-an-app.md) on GitHub. + +## What do I get? (Let's get technical...) + +We’ve been using Backstage internally for years, and we’ve spent a lot of time adding to and tweaking the infrastructure so that it fits our needs. After all that testing and trial and error, we think it will fit your needs, too! + +### 1. Lerna setup to manage multi-packages + +The monorepo and its packages are managed by [Lerna](https://lerna.js.org/). It lets you work with individual packages in a controlled way. + +### 2. Fast builds + +Behind the scenes we use [Rollup](https://rollupjs.org/) to build the modules. + +Each package is built individually. With the `--watch` flag you will be able to detect changes per package and therefore speed up the local development process. + +To further speed things up, we have also included our own caching system to avoid rebuilding unchanged packages. + +Our hope is that there will be thousands of Backstage plugins in the future, so we need a fast and stable build process. + +### 3. Full TypeScript support + +Most of the codebase is written in [TypeScript](https://www.typescriptlang.org/), and we aim for all of the core packages to be in TypeScript in the future. + +All the knobs and handles needed for a stable and functioning TypeScript project are included. + +Take a look at `@backstage/cli/config/tsconfig.json` for more details. + +### 4. Tests and coverage out of the box + +We include testing, linting, and end-to-end tests for your convenience. + +```bash +yarn lint:all +yarn test:all +yarn test:e2e +``` + +## Extend the app with plugins + +At Spotify, the main factor behind Backstage’s success has been our large and diverse collection of plugins — the result of contributions from various teams over the years. Internally, we have more than a hundred different plugins. + +There are two ways to add plugins to your Backstage app: use a publicly available plugin or create your own. + +### Using a public plugin + +We provide a collection of public Backstage plugins (look for packages with the `plugin-` prefix under the `@backstage` namespace on [npm](https://www.npmjs.com/) that you can start using immediately. + +Install in your app’s package folder (`/packages/app`) with: + +```bash +yarn add @backstage/plugin- +``` + +Then add it to your app's `plugin.ts` file to import and register it: + +`/packages/app/src/plugin.ts`: + +```js +export { plugin as PluginName } from '@backstage/plugin-'; +``` + +A plugin registers its own `route` in the app — read the documentation for the specific plugin you are installing for more information on that. + +### Creating an internal plugin + +We also know that each organization has different needs and will create their own plugins for internal purposes. To create an internal plugin, you can use our CLI again. + +In the root of your app directory (``) run: + +```bash +yarn create-plugin +``` + +This command will create a new plugin in `/plugins/` and register it to your app automatically. + +### Sharing is caring 🤗 + +If you are developing a plugin that might be useful for others, consider releasing it publicly. A large, diverse ecosystem of Backstage plugins benefits the whole community + +## Ready to get started? + +Head over to GitHub and check out the [project](https://github.com/spotify/backstage) or download our [CLI](https://www.npmjs.com/package/@backstage/cli). If you have more questions, join us on [Discord](https://discord.gg/MUpMjP2) or [create an issue](https://github.com/spotify/backstage/issues/new/choose). diff --git a/microsite/blog/2020-05-14-tech-radar-plugin.md b/microsite/blog/2020-05-14-tech-radar-plugin.md new file mode 100644 index 0000000000..d78d5d67fa --- /dev/null +++ b/microsite/blog/2020-05-14-tech-radar-plugin.md @@ -0,0 +1,44 @@ +--- +title: Introducing Tech Radar for Backstage +author: Bilawal Hameed +authorURL: http://twitter.com/bilawalhameed +authorImageURL: https://avatars0.githubusercontent.com/bih +--- + +![image illustrating the Tech Radar plugin for Backstage](assets/5/lead.png) + +Just a few weeks ago, we released our internal plugin for [Lighthouse website audits] as our first open source plugin, so the whole community could use it. Today, we’re excited to add a new plugin to that list — say hello to the [Tech Radar plugin]! + + + +## What is Tech Radar? + +The Technology Radar is a concept created by [ThoughtWorks] which allows you to visualize the official guidelines of software languages, processes, infrastructure, and platforms at that particular company. The particular visualization above was created by [Zalando]. + +At Spotify, our central committee of technical architects own the Tech Radar with the input of engineers across the company. Anyone can and is encouraged to give recommendations. We segment entries in our Tech Radar by languages, frameworks, processes, and infrastructure, although you should pick whatever works best for your organization. Each entry in the Tech Radar can have one of the following lifecycle values: Use, Trial, Assess, and Hold. + +We also assign clear definitions for each lifecycle: + +- **Use:** This technology is recommended for use by the majority of teams with a specific use case. +- **Trial:** This technology has been evaluated for specific use cases and has showed clear benefits. Some teams adopt it in production, although it should be limited to low-impact projects as it might incur a higher risk. +- **Assess:** This technology has the potential to be beneficial for the company. Some teams are evaluating it and using it in experimental projects. Using it in production comes with a high cost and risk due to lack of in-house knowledge, maintenance, and support. +- **Hold:** We don’t want to further invest in this technology or we evaluated it and we don’t see it as beneficial for the company. Teams should not use it in new projects and should plan on migrating to a supported alternative if they use it for historical reasons. For broadly adopted technologies, the Radar should refer to a migration path to a supported alternative. + +Since rolling out the Tech Radar, it has become the source of truth when creating, maintaining, or evolving our software ecosystem. Spotify has dozens of entries in our Radar and it can scale quite well whilst being easy for our engineers and engineering managers to consume. + +## Using the Tech Radar in Backstage + +To learn about how you can bring the Tech Radar to your Backstage installation, check out [the plugin README on GitHub][tech radar plugin]. + +## A personal note + +I want to thank both the Backstage team and Spotify. Firstly, I’ve been working with our internal version of Backstage for over a year, and the developer experience since open sourcing has been even more of a joy to work with. Secondly, the 10% hack time that Spotify generously provides to all engineers enabled me to open source the Tech Radar plugin. + +Since open sourcing it, the community has shown great interest in yet another powerful use case of Backstage. There was also an enthusiastic open source contributor who volunteered to migrate the plugin to TypeScript and React Hooks [in just 29 minutes](https://github.com/spotify/backstage/issues/661) of opening the issue! + +I can’t wait to see how others benefit from the Tech Radar in their organizations! + +[lighthouse website audits]: https://backstage.io/blog/2020/04/06/lighthouse-plugin +[tech radar plugin]: https://github.com/spotify/backstage/tree/master/plugins/tech-radar +[thoughtworks]: https://www.thoughtworks.com/radar +[zalando]: https://opensource.zalando.com/tech-radar/ diff --git a/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md b/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md new file mode 100644 index 0000000000..68e3f903c7 --- /dev/null +++ b/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md @@ -0,0 +1,33 @@ +--- +title: Weaveworks’ COVID-19 app uses Backstage UI +author: Jeff Feng +authorURL: https://github.com/fengypants +authorImageURL: https://avatars2.githubusercontent.com/u/46946747 +--- + +![fk-covid-screenshot](assets/20-05-14/weaveworks-firekube-covid-19-spotify-backstage.png) + +One of the great things about the open source community is once you put your work out there, you really never know where it might end up. That’s certainly the case here. + + + +When Weaveworks decided to build an X-ray diagnostics app to help fight COVID-19, they pulled together a lot of different open source resources — from projects big and small, built by both familiar tech partners and some brand new ones, too. + +At the heart of their app — called [fk-covid][] — there’s a TensorFlow-based deep neural network that was developed by researchers on the DarwinAI team and others in the COVID R&D community. To package that network up for doctors and software developers to use, the app combines open source tools from Google, AWS, Azure, MinIO, the CNCF, and Weaveworks’ own Firekube bundle for Kubernetes. + +And the user interface for all of this? Weaveworks built a custom plugin using the Backstage framework. + +“We chose Backstage as a modern UI toolkit that we knew would work with Kubernetes apps,” said Alexis Richardson, CEO of Weaveworks. “We were also experimenting with Backstage for microservices and ML, so it was natural to try it here.” + +Chanwit Kaewkasi, Weaveworks’ DX Engineer and a tech lead on the project, said, “Backstage offers very advanced plugin architecture which allows us to only focus on the plugin we're developing. Other things are taken care of by the framework.” + +In other words, here’s Backstage doing what Backstage does best: unifying a bunch of technologies with a cohesive frontend, so that the whole thing is easier to build and easier to use. + +Joining the fight against a global pandemic was not something the Backstage team at Spotify ever envisioned when we released our homegrown developer portal to the world back in March. But it’s a testament to the ingenuity (and serendipity) of the open source community that Backstage could be enlisted for such an unexpected use case. + +We’re proud to see Backstage adopted as the UX layer for this meaningful cause. And we can’t wait to see what the open source community will build next. + +To learn more about what fk-covid does, and how it works, jump on over to [the Weaveworks blog][] to hear it straight from the team that built it. It’s a great example of the possibilities that come from being a part of the open source community. + +[fk-covid]: https://github.com/weaveworks/fk-covid +[the weaveworks blog]: https://www.weave.works/blog/firekube-covid-ml diff --git a/microsite/blog/2020-05-22-phase-2-service-catalog.md b/microsite/blog/2020-05-22-phase-2-service-catalog.md new file mode 100644 index 0000000000..682965d2dd --- /dev/null +++ b/microsite/blog/2020-05-22-phase-2-service-catalog.md @@ -0,0 +1,54 @@ +--- +title: Starting Phase 2: The Service Catalog +author: Stefan Ålund +authorURL: http://twitter.com/stalund +authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg +--- + +**TL;DR** Thanks to the help from the Backstage community, we’ve made excellent progress and are now moving into Phase 2 of Backstage — building out a Service Catalog and the surrounding systems that will help unify the tools you use to manage your software. + +We released the open source version of Backstage a little less than two months ago, and have been thrilled to see so many people jumping in and contributing to the project in its early stages. We’re excited to see what the community can build together as we progress through [each phase of Backstage](https://github.com/spotify/backstage#project-roadmap). + +![img](assets/20-05-20/Service_Catalog_MVP.png) + + + +## Progress so far + +Phase 1 was all about building an extensible frontend platform, enabling teams to start creating a single, consistent UI layer for your internal infrastructure and tools in the form of [plugins](https://github.com/spotify/backstage/labels/plugin). In fact, thanks to our amazing (30+) [contributors](https://github.com/spotify/backstage/graphs/contributors), we were able to complete most of Phase 1 earlier than expected. 🎉 + +Today, we are happy to announce that we are shifting our focus to Phase 2! + +## So what is Phase 2? + +> _The core of building Platforms rests in versatile entity management. Entities represent the nouns or the "truths" of our world._ + +Quote from [Platform Nuts & Bolts: Extendable Data Models](https://www.kislayverma.com/post/platform-nuts-bolts-extendable-data-models) + +Entities, or what we refer to as “components” in Backstage, represent all software, including services, websites, libraries, data pipelines, and so forth. The focus of Phase 2 will be on adding an entity model in Backstage that makes it easy for engineers to create and manage the software components they own. + +With the ability to create a plethora of components in Backstage, how does one keep track of all the software in the ecosystem? Therein lies the highlight feature of Phase 2: the [Service Catalog](https://github.com/spotify/backstage/milestone/4). The service catalog — or software catalog — is a centralized system that keeps track of ownership and metadata about all software in your ecosystem. The catalog is built around the concept of [metadata yaml files](/docs/architecture-decisions/adr002-default-catalog-file-format.md) stored together with the code, which are then harvested and visualized in Backstage. + +![img](assets/20-05-20/Service_Catalog_MVP.png) + +![img](assets/20-05-20/Service_Catalog_MVP_service.png) + +With a single catalog, Backstage makes it easy for a team to manage ten services — and makes it possible for your company to manage thousands of them. Because the system is practically self-organizing, it requires hardly any oversight from a governing or centralized team. Developers can get a uniform overview of all their software and related resources (such as server utilisation, data pipelines, pull request status), regardless of how and where they are running, as well as an easy way to onboard and manage those resources. + +On top of that, we have found that the service catalog is a great way to organise the infrastructure tools you use to manage the software as well. This is how Backstage creates one developer portal for all your tools. Rather than asking teams to jump between different infrastructure UI’s (and incurring additional cognitive overhead each time they make a context switch), most of these tools can be organised around the entities in the catalog: + +![img](assets/20-05-20/tabs.png) + +More concretely, having this structure in place will allow plugins such as [CircleCI](https://github.com/spotify/backstage/tree/master/plugins/circleci) to show only the builds for the specific service you are viewing, or a [Spinnaker](https://github.com/spotify/backstage/issues/631) plugin to show running deployments, or an Open API plugin to [show documentation](https://github.com/spotify/backstage/issues/627) for endpoints exposed by the service, or the [Lighthouse](https://github.com/spotify/backstage/tree/master/plugins/lighthouse) plugin to show audit reports for your website. You get the point. + +## Timeline + +Our estimated timeline has us delivering these pieces in increments leading up to June 22. But with the support of the community we wouldn’t be surprised if things land earlier than that. 🙏 + +If you are interested in joining us, check out our [Milestones](https://github.com/spotify/backstage/milestones) and connected Issues. + +## Long-term vision + +Our vision for Backstage is for it to become the trusted, standard toolbox (read: UX layer) for the open source infrastructure landscape. Imagine a future where regardless of what infrastructure you use inside your company, there is an open source plugin available that you can pick up and add to your deployment of Backstage. + +Spotify will continue to release more of our [internal](https://backstage.io/blog/2020/04/06/lighthouse-plugin) [plugins](https://backstage.io/blog/2020/05/14/tech-radar-plugin), but participation from developers and companies can help us build a healthy community. We are excited to see how Backstage has helped many of you, and look forward to seeing all the new plugins you and your teams will build! diff --git a/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md b/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md new file mode 100644 index 0000000000..0928b8769b --- /dev/null +++ b/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md @@ -0,0 +1,54 @@ +--- +title: Backstage Service Catalog released in alpha +author: Stefan Ålund +authorURL: http://twitter.com/stalund +image: https://backstage.io/blog/assets/6/header.png +--- + +**TL;DR** Today we are announcing the availability of the Backstage Service Catalog in alpha. This has been the community’s most requested feature. Even if the catalog is not ready for production yet, we think this release already demonstrates how Backstage can provide value for your company right out of the box. With your early input and feedback, we hope to create a stronger generally available product. + +![img](assets/6/header.png) + + + +## You asked, we listened + +When we [released](https://backstage.io/blog/2020/03/16/announcing-backstage) Backstage as an open source project back in March, it didn’t have all of the features that our internal version of Backstage has today. One of the main reasons we pushed to release it, despite it being in such a nascent stage, was so that we could start building the next phase of Backstage around the community’s needs. We’ve had hours of conversations with so many of you — thank you to everyone who has jumped on a video call, attended one of our working sessions, or watched our [demo videos](https://backstage.io/demos) and provided feedback via [Discord](https://discord.com/invite/MUpMjP2). + +Today, we wanted to share what we’ve learned from talking with many of you at companies that have shown interest in adopting Backstage. Here it is in short: + +- The problem of scaling autonomous engineering organisations without creating too much complexity is not a unique problem to Spotify. +- The "extensible frontend platform" that we focused on in the first phase of the project is not the only thing you are looking for. + +With these insights we decided to re-focus our efforts towards the most requested feature: the Backstage Service Catalog. + +## What is the service catalog? + +The Backstage Service Catalog — actually, a software catalog, since it includes more than just services — is a centralized system that keeps track of ownership and metadata for all the software in your ecosystem (services, websites, libraries, data pipelines, etc). The catalog is built around the concept of [metadata yaml files](/docs/architecture-decisions/adr002-default-catalog-file-format.md#format) stored together with the code, which are then harvested and visualized in Backstage. + +This was our pitch for the virtues of a service catalog when we first [announced](https://backstage.io/blog/2020/05/22/phase-2-service-catalog) it as part of Phase 2: + +> With a single catalog, Backstage makes it easy for a team to manage ten services — and makes it possible for your company to manage thousands of them. Because the system is practically self-organizing, it requires hardly any oversight from a governing or centralized team. Developers can get a uniform overview of all their software and related resources (such as server utilisation, data pipelines, pull request status), regardless of how and where they are running, as well as an easy way to onboard and manage those resources. + +> On top of that, we have found that the service catalog is a great way to organise the infrastructure tools you use to manage the software as well. This is how Backstage creates one developer portal for all your tools. Rather than asking teams to jump between different infrastructure UI’s (and incurring additional cognitive overhead each time they make a context switch), most of these tools can be organised around the entities in the catalog: + +![img](assets/20-05-20/tabs.png) + +You’ll be able to see many of these virtues in action with this alpha release — though, with some caveats, of course, since it is, after all, an alpha. + +## What does alpha mean? + +Alpha is our shorthand for "we don’t yet think Backstage is ready for production, but we’d love for you to test it and provide us with feedback". However, you should be able to try out the functionality of the service catalog: + +1. Register software components ([examples](https://github.com/spotify/backstage/tree/master/packages/catalog-model/examples)) +2. See all components represented in the catalog +3. Search across all components +4. Get an overview of the metadata of the components +5. Click through and get more information about a specific component (service, website, etc) +6. See example tooling (plugins) that helps you manage the component + +As with most alpha releases, you should expect things to change quite a lot until we reach the beta stage (we’re targeting the end of summer). There are obviously many things missing as well, but we wanted to start collecting feedback early and make it easier to see the end-to-end flow. + +If you have feedback or questions, please open a [GitHub issue](https://github.com/spotify/backstage/issues), ping us on [Discord chat](https://discord.gg/EBHEGzX) or send me an email at [alund@spotify.com](mailto:alund@spotify.com) 🙏 + +To get regular product updates and news about the Backstage community, sign up for the [Backstage newsletter](https://mailchi.mp/spotify/backstage-community). diff --git a/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md b/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md new file mode 100644 index 0000000000..ccdd8b34c6 --- /dev/null +++ b/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md @@ -0,0 +1,46 @@ +--- +title: How to enable authentication in Backstage using Passport +author: Lee Mills +authorURL: https://github.com/leemills83 +authorImageURL: https://avatars1.githubusercontent.com/u/1236238?s=460&v=4 +--- + +![auth-landing-page](assets/20-07-01/auth-landing.png) + +Getting authentication right is important. It helps keep your platform safe, it’s one of the first things users will interact with, and there are many different authentication providers to support. To this end, we chose to use [Passport](http://www.passportjs.org/) to provide an easy-to-use, out-of-the-box experience that can be extended to your own, pre-existing authentication providers (known as strategies). The Auth APIs in Backstage serve two purposes: identify the user and provide a way for plugins to request access to third-party services on behalf of the user. We’ve already implemented Google and GitHub authentication to provide examples and to get you started. + + + +## What is Passport? + +[Passport](http://www.passportjs.org/) is Express-compatible authentication middleware for Node.js that provides access to over 500 authentication providers, covering everything from Google, Facebook, and Twitter to generic OAuth, SAML, and local. Check out all of the currently available [strategies listed on the Passport site](http://www.passportjs.org/). + +Passport has allowed us to leverage an existing open-source authentication framework that will, in turn, give users the freedom to add and extend alternative authentication strategies to their instance of Backstage. + +## Using authentication in Backstage + +![auth-landing-page](assets/20-07-01/auth-sidebar.png) + +First, check out the provided Google and GitHub implementations! [Spin up a local copy of Backstage](https://backstage.io/blog/2020/04/30/how-to-quickly-set-up-backstage) along with our example-backend. You can find more documentation on setting up the example backend [here](https://github.com/spotify/backstage/tree/master/packages/backend), but be sure to include the relevant client IDs and secrets when running `yarn start`: + +``` +AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x SENTRY_TOKEN=x LOG_LEVEL=debug yarn start +``` + +You can find the implementation for these strategies along with a lightweight proof-of-concept implementation for SAML authentication at `/plugins/auth-backend/src/providers`. + +## Ready to get started by adding your chosen provider and implementation? + +Getting started is really straightforward, and can be broadly broken down into five steps: + +1. Install the [Passport-based provider package that best suits your needs](http://www.passportjs.org/). +2. Add a new provider to `plugins/auth-backend/src/providers/` +3. Implement the provider, extending the suitable framework, if needed. +4. Add the provider to the backend. +5. Add a frontend Auth Utility API. + +For full details, take a look at our [“Adding authentication providers” documentation](/docs/auth/add-auth-provider.md) and at the [excellent documentation](http://www.passportjs.org/docs/) provided by Passport. + +## Interested in contributing to the next steps for authentication? + +We’ve already seen both GitLab and Okta contributions from the community — and we’re thinking about a few more providers we’d like to add to Backstage, too. You can find those, and other authentication-related issues, in our repository by filtering with the [auth label](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aauth). diff --git a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md new file mode 100644 index 0000000000..51c1da2885 --- /dev/null +++ b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md @@ -0,0 +1,78 @@ +--- +title: Announcing Backstage Software Templates +author: Stefan Ålund +authorURL: https://twitter.com/stalund +--- + +**TL;DR** Today we are announcing a new Backstage feature: Software Templates. Simplify setup, standardize tooling, and deploy with the click of a button. Using automated templates, your engineers can spin up a new microservice, website, or other software component with your organization’s best practices built-in, right from the start. + + + + + +## Balancing autonomy and standardization + +At Spotify, we’ve always believed in the speed and ingenuity that comes from having autonomous development teams. But as we learned firsthand, the faster you grow, the more fragmented and complex your software ecosystem becomes. And then everything slows down again. + +By centralizing services and standardizing your tooling, Backstage streamlines your development environment from end to end. Instead of restricting autonomy, standardization frees your engineers from infrastructure complexity. So you can return to building and scaling, quickly and safely. + +Today we are releasing one of the key features that helps balance autonomy and standardization: templates for creating software. + +## Backstage Software Templates: Push-button deployment + +Backstage Software Templates automate and standardize the process of creating software components. To show you how they work, we created four sample templates to get you started — just configure them to fit your tooling and off you go: + +- **Create React App Template** — create a new CRA website project +- **Golang Microservice** — create a Golang repo with this template built by members of the Go community +- **React SSR Template** — create a website powered with Next.js +- **Spring Boot GRPC** — create a simple microservice using gRPC and Spring Boot Java + +### The getting started guide gets automated + +Since the templates can be customized to integrate with your existing infrastructure, it’s easy to start a new project without ever having to leave Backstage. Let’s say you’re building a microservice. With three clicks in Backstage, you’ll have a new Spring Boot project with your repo automatically configured on GitHub and your CI already running the first build. + +### Golden Paths pave the way + +You can customize Backstage Software Templates to fit your organization’s standards. Using Go instead of Java? CircleCI instead of Jenkins? Serverless instead of Kubernetes? GCP instead of AWS? [Make your own recipes for any software component](https://backstage.io/docs/features/software-templates/adding-templates) and your best practices will be baked right in. + +## Getting started + +The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](https://backstage.io/docs/getting-started/) and go to `http://localhost:3000/create`. If you’ve already been running Backstage locally, run the command `yarn lerna run mock-data` to load the new sample templates into the Service Catalog first. + +![available-templates](assets/2020-08-05/templates.png) + +### Step 1: Choose a template + +When you select a template that you want to create, you can ask for different input variables. These are then passed to the templater internally. + +![template-form](assets/2020-08-05/template-form.png) + +After filling in these variables, additional fields will appear so Backstage can be used. You’ll specify the owner, which is a `user` in the Backstage system, and the `Location`, which must be a GitHub organization and a non-existing GitHub repository name, formatted as `organization/reponame`. + +### Step 2: Run! + +Once you've entered values and confirmed, you'll then get a modal with live progress of what is currently happening with the creation of your template. + +![create-component](assets/2020-08-05/create-component.png) + +It shouldn't take too long before you see a success screen. At this point, a piece of “Hello World” software has been created in your repo, and the CI automatically picks it up and starts building the code. + +Your engineers don’t have to bother with setting up underlying infrastructure, it’s all built into the template. They can start focusing on delivering business value. + +### View new components in the Service Catalog + +New components, of course, get added automatically to the Backstage Service Catalog. After creation, you'll see the `View in Catalog` button, which will take you to the registered component in the catalog: + +![service-catalog](assets/2020-08-05/catalog.png) + +## Define your standards + +Backstage ships with four example templates, but since these are likely not the (only) ones you want to promote inside your company, the next step is to add [your own templates](https://backstage.io/docs/features/software-templates/software-templates-index). Using Backstage’s Software Templates feature, it’s easy to help your engineers get started building software with your organization’s best practices built-in. + +We have learned that one of the keys to getting these standards adopted is to keep an open process. Templates are code. By making it clear to your engineers that you are open to pull requests, and that teams with different needs can add their own templates, you are on the path of striking a good balance between autonomy and standardization. + +If you have feedback or questions, please open a [GitHub issue](https://github.com/spotify/backstage/issues), ping us on [Discord chat](https://discord.gg/EBHEGzX) or send me an email at [alund@spotify.com](mailto:alund@spotify.com) 🙏 + +To get regular product updates and news about the Backstage community, sign up for the [Backstage newsletter](https://mailchi.mp/spotify/backstage-community). diff --git a/microsite/blog/assets/2/1.png b/microsite/blog/assets/2/1.png new file mode 100644 index 0000000000..21fb8bc26f Binary files /dev/null and b/microsite/blog/assets/2/1.png differ diff --git a/microsite/blog/assets/2/2.png b/microsite/blog/assets/2/2.png new file mode 100644 index 0000000000..16fa0cf5bf Binary files /dev/null and b/microsite/blog/assets/2/2.png differ diff --git a/microsite/blog/assets/2/3.png b/microsite/blog/assets/2/3.png new file mode 100644 index 0000000000..f610782186 Binary files /dev/null and b/microsite/blog/assets/2/3.png differ diff --git a/microsite/blog/assets/2/4.png b/microsite/blog/assets/2/4.png new file mode 100644 index 0000000000..d5e59ef92a Binary files /dev/null and b/microsite/blog/assets/2/4.png differ diff --git a/microsite/blog/assets/2/5.png b/microsite/blog/assets/2/5.png new file mode 100644 index 0000000000..00a6163c9a Binary files /dev/null and b/microsite/blog/assets/2/5.png differ diff --git a/microsite/blog/assets/2/screen.gif b/microsite/blog/assets/2/screen.gif new file mode 100644 index 0000000000..a0d52a23b4 Binary files /dev/null and b/microsite/blog/assets/2/screen.gif differ diff --git a/microsite/blog/assets/2/spotify-labs-header.png b/microsite/blog/assets/2/spotify-labs-header.png new file mode 100644 index 0000000000..8effd4b05a Binary files /dev/null and b/microsite/blog/assets/2/spotify-labs-header.png differ diff --git a/microsite/blog/assets/20-05-14/weaveworks-firekube-covid-19-spotify-backstage.png b/microsite/blog/assets/20-05-14/weaveworks-firekube-covid-19-spotify-backstage.png new file mode 100644 index 0000000000..fc5d05cd5d Binary files /dev/null and b/microsite/blog/assets/20-05-14/weaveworks-firekube-covid-19-spotify-backstage.png differ diff --git a/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png b/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png new file mode 100644 index 0000000000..4a50f17d15 Binary files /dev/null and b/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png differ diff --git a/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png b/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png new file mode 100644 index 0000000000..8067f665c9 Binary files /dev/null and b/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png differ diff --git a/microsite/blog/assets/20-05-20/tabs.png b/microsite/blog/assets/20-05-20/tabs.png new file mode 100644 index 0000000000..4a5b11f6fe Binary files /dev/null and b/microsite/blog/assets/20-05-20/tabs.png differ diff --git a/microsite/blog/assets/20-07-01/auth-landing.png b/microsite/blog/assets/20-07-01/auth-landing.png new file mode 100644 index 0000000000..e00f4c2e43 Binary files /dev/null and b/microsite/blog/assets/20-07-01/auth-landing.png differ diff --git a/microsite/blog/assets/20-07-01/auth-sidebar.png b/microsite/blog/assets/20-07-01/auth-sidebar.png new file mode 100644 index 0000000000..b089ab6c0a Binary files /dev/null and b/microsite/blog/assets/20-07-01/auth-sidebar.png differ diff --git a/microsite/blog/assets/2020-08-05/cards.png b/microsite/blog/assets/2020-08-05/cards.png new file mode 100644 index 0000000000..4779618b92 Binary files /dev/null and b/microsite/blog/assets/2020-08-05/cards.png differ diff --git a/microsite/blog/assets/2020-08-05/catalog.png b/microsite/blog/assets/2020-08-05/catalog.png new file mode 100644 index 0000000000..e9c0c65ade Binary files /dev/null and b/microsite/blog/assets/2020-08-05/catalog.png differ diff --git a/microsite/blog/assets/2020-08-05/create-component.png b/microsite/blog/assets/2020-08-05/create-component.png new file mode 100644 index 0000000000..4d815393fc Binary files /dev/null and b/microsite/blog/assets/2020-08-05/create-component.png differ diff --git a/microsite/blog/assets/2020-08-05/feature.mp4 b/microsite/blog/assets/2020-08-05/feature.mp4 new file mode 100644 index 0000000000..a42d9da0e6 Binary files /dev/null and b/microsite/blog/assets/2020-08-05/feature.mp4 differ diff --git a/microsite/blog/assets/2020-08-05/template-form.png b/microsite/blog/assets/2020-08-05/template-form.png new file mode 100644 index 0000000000..5805243f59 Binary files /dev/null and b/microsite/blog/assets/2020-08-05/template-form.png differ diff --git a/microsite/blog/assets/2020-08-05/templates.png b/microsite/blog/assets/2020-08-05/templates.png new file mode 100644 index 0000000000..e350d463f6 Binary files /dev/null and b/microsite/blog/assets/2020-08-05/templates.png differ diff --git a/microsite/blog/assets/3/audit-list.png b/microsite/blog/assets/3/audit-list.png new file mode 100644 index 0000000000..84b64f976a Binary files /dev/null and b/microsite/blog/assets/3/audit-list.png differ diff --git a/microsite/blog/assets/3/audit-view.png b/microsite/blog/assets/3/audit-view.png new file mode 100644 index 0000000000..a2e9716cf4 Binary files /dev/null and b/microsite/blog/assets/3/audit-view.png differ diff --git a/microsite/blog/assets/3/create-audit.png b/microsite/blog/assets/3/create-audit.png new file mode 100644 index 0000000000..cb28de73a4 Binary files /dev/null and b/microsite/blog/assets/3/create-audit.png differ diff --git a/microsite/blog/assets/3/lead-copy.png b/microsite/blog/assets/3/lead-copy.png new file mode 100644 index 0000000000..fbf247eec8 Binary files /dev/null and b/microsite/blog/assets/3/lead-copy.png differ diff --git a/microsite/blog/assets/3/lead.png b/microsite/blog/assets/3/lead.png new file mode 100644 index 0000000000..4b60c4961c Binary files /dev/null and b/microsite/blog/assets/3/lead.png differ diff --git a/microsite/blog/assets/4/create-app.png b/microsite/blog/assets/4/create-app.png new file mode 100644 index 0000000000..52dcc13097 Binary files /dev/null and b/microsite/blog/assets/4/create-app.png differ diff --git a/microsite/blog/assets/4/welcome.png b/microsite/blog/assets/4/welcome.png new file mode 100644 index 0000000000..5de0d57098 Binary files /dev/null and b/microsite/blog/assets/4/welcome.png differ diff --git a/microsite/blog/assets/5/lead.png b/microsite/blog/assets/5/lead.png new file mode 100644 index 0000000000..657268fc09 Binary files /dev/null and b/microsite/blog/assets/5/lead.png differ diff --git a/microsite/blog/assets/6/header.png b/microsite/blog/assets/6/header.png new file mode 100644 index 0000000000..6908e40dbc Binary files /dev/null and b/microsite/blog/assets/6/header.png differ diff --git a/microsite/blog/assets/blog_1.png b/microsite/blog/assets/blog_1.png new file mode 100644 index 0000000000..f8c3516fa7 Binary files /dev/null and b/microsite/blog/assets/blog_1.png differ diff --git a/microsite/blog/assets/illustration.svg b/microsite/blog/assets/illustration.svg new file mode 100644 index 0000000000..50e865ed4f --- /dev/null +++ b/microsite/blog/assets/illustration.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/microsite/core/Components.js b/microsite/core/Components.js new file mode 100644 index 0000000000..49e7d7daca --- /dev/null +++ b/microsite/core/Components.js @@ -0,0 +1,114 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const simpleComponent = (Component, baseClassName = '', mods = []) => { + const SimpleComponent = props => { + // Extra BEM modifiers, e.g. `Block__Container--reversed` + const modClasses = []; + const otherProps = {}; + for (const prop in props) { + if (mods.indexOf(prop) !== -1) { + modClasses.push(`${baseClassName}--${prop}`); + } else { + otherProps[prop] = props[prop]; + } + } + + return ( + + ); + }; + SimpleComponent.displayName = `SimpleComponent(${Component}, ${baseClassName})`; + + SimpleComponent.propTypes = {}; + for (const mod of mods) { + SimpleComponent.propTypes[mod] = PropTypes.bool; + } + + return SimpleComponent; +}; + +const Block = simpleComponent('section', 'Block', ['small', 'wrapped']); +Block.Container = simpleComponent('div', 'Block__Container', [ + 'reversed', + 'wrapped', + 'column', +]); +Block.TitleBox = simpleComponent('h1', 'Block__TitleBox', ['large', 'story']); +Block.TextBox = simpleComponent('div', 'Block__TextBox', ['wide', 'small']); + +Block.Title = simpleComponent('h1', 'Block__Title', ['half', 'main']); +Block.Subtitle = simpleComponent('h1', 'Block__Subtitle'); + +Block.SmallTitle = simpleComponent('h2', 'Block__SmallTitle'); +Block.SmallestTitle = simpleComponent('h3', 'Block__SmallestTitle'); + +const BulletLine = simpleComponent('div', 'BulletLine'); + +Block.Paragraph = simpleComponent('p', 'Block__Paragraph'); +Block.LinkButton = simpleComponent('a', 'Block__LinkButton', ['stretch']); +Block.QuoteContainer = simpleComponent('div', 'Block__QuoteContainer'); +Block.Quote = simpleComponent('p', 'Block__Quote'); +Block.Divider = simpleComponent('p', 'Block__Divider', ['quote']); +Block.MediaFrame = simpleComponent('div', 'Block__MediaFrame'); +Block.Graphics = ({ padding, children }) => { + const style = {}; + if (padding) { + style.padding = `${padding}% 0`; + } + return ( +
+
+
+ ); +}; +Block.Graphic = props => { + /* Coordinates and size are in % of graphics container size, e.g. width={50} is 50% of parent width */ + const { x = 0, y = 0, width = 0, src, className = '' } = props; + const style = Object.assign( + { left: `${x}%`, top: `${y}%`, width: `${width}%` }, + props.style, + ); + return ( + + ); +}; + +Block.Image = props => { + /* Coordinates and size are in % of graphics container size, e.g. width={50} is 50% of parent width */ + return ( +
+ ); +}; + +const ActionBlock = simpleComponent('section', 'ActionBlock'); +ActionBlock.Title = simpleComponent('h1', 'ActionBlock__Title'); +ActionBlock.Subtitle = simpleComponent('h2', 'ActionBlock__Subtitle'); +ActionBlock.Link = simpleComponent('a', 'ActionBlock__Link'); + +const Breakpoint = ({ narrow, wide }) => ( + +
{narrow}
+
{wide}
+
+); + +module.exports = { + Block, + ActionBlock, + Breakpoint, + BulletLine, +}; diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js new file mode 100644 index 0000000000..761862c1fc --- /dev/null +++ b/microsite/core/Footer.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); + +class Footer extends React.Component { + docUrl(doc, language) { + const baseUrl = this.props.config.baseUrl; + const docsUrl = this.props.config.docsUrl; + const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; + const langPart = `${language ? `${language}/` : ''}`; + return `${baseUrl}${docsPart}${langPart}${doc}`; + } + + pageUrl(doc, language) { + const baseUrl = this.props.config.baseUrl; + return baseUrl + (language ? `${language}/` : '') + doc; + } + + render() { + return ( + + ); + } +} + +module.exports = Footer; diff --git a/microsite/core/GridBlockWithButton.js b/microsite/core/GridBlockWithButton.js new file mode 100644 index 0000000000..0f77a6a148 --- /dev/null +++ b/microsite/core/GridBlockWithButton.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const classNames = require('classnames'); + +const CompLibrary = require(`${process.cwd()}/node_modules/docusaurus/lib/core/CompLibrary.js`); +const MarkdownBlock = CompLibrary.MarkdownBlock; /* Used to read markdown */ + +class GridBlockWithButton extends React.Component { + renderBlock(origBlock) { + const blockDefaults = { + imageAlign: 'left', + }; + + const block = { + ...blockDefaults, + ...origBlock, + }; + + const blockClasses = classNames('blockElement', this.props.className, { + alignCenter: this.props.align === 'center', + alignRight: this.props.align === 'right', + fourByGridBlock: this.props.layout === 'fourColumn', + imageAlignSide: + block.image && + (block.imageAlign === 'left' || block.imageAlign === 'right'), + imageAlignTop: block.image && block.imageAlign === 'top', + imageAlignRight: block.image && block.imageAlign === 'right', + imageAlignBottom: block.image && block.imageAlign === 'bottom', + imageAlignLeft: block.image && block.imageAlign === 'left', + threeByGridBlock: this.props.layout === 'threeColumn', + twoByGridBlock: this.props.layout === 'twoColumn', + }); + + const topLeftImage = + (block.imageAlign === 'top' || block.imageAlign === 'left') && + this.renderBlockImage(block.image, block.imageLink, block.imageAlt); + + const bottomRightImage = + (block.imageAlign === 'bottom' || block.imageAlign === 'right') && + this.renderBlockImage(block.image, block.imageLink, block.imageAlt); + + return ( +
+ {topLeftImage} +
+ {this.renderBlockTitle(block.title)} + {block.content} + +
+ {bottomRightImage} +
+ ); + } + + renderBlockImage(image) { + if (!image) { + return null; + } + + return
{image}
; + } + + renderBlockTitle(title) { + if (!title) { + return null; + } + + return ( +

+ {title} +

+ ); + } + + render() { + return ( +
+ {this.props.contents.map(this.renderBlock, this)} +
+ ); + } +} + +GridBlockWithButton.defaultProps = { + align: 'left', + contents: [], + layout: 'twoColumn', +}; + +module.exports = GridBlockWithButton; diff --git a/microsite/data/plugins/api-docs.yaml b/microsite/data/plugins/api-docs.yaml new file mode 100644 index 0000000000..babcbf74f1 --- /dev/null +++ b/microsite/data/plugins/api-docs.yaml @@ -0,0 +1,9 @@ +--- +title: API Docs +author: SDA SE +authorUrl: https://sda.se/ +category: Discovery +description: Components to discover and display API entities as an extension to the catalog plugin. +documentation: https://github.com/spotify/backstage/blob/master/plugins/api-docs/README.md +iconUrl: https://thecoders.io/wp-content/uploads/2019/11/tech-swagger.svg +npmPackageName: '@backstage/plugin-api-docs' diff --git a/microsite/data/plugins/circleci.yaml b/microsite/data/plugins/circleci.yaml new file mode 100644 index 0000000000..5f7b4b08e2 --- /dev/null +++ b/microsite/data/plugins/circleci.yaml @@ -0,0 +1,12 @@ +--- +title: CircleCI +author: Spotify +authorUrl: https://github.com/spotify +category: CI +description: Automate your development process with CI hosted in the cloud or on a private server. +documentation: https://github.com/spotify/backstage/tree/master/plugins/circleci +iconUrl: https://www.saaves.com/storage/brochure/logo-circleci-icon1583764538.png +npmPackageName: '@backstage/plugin-circleci' +tags: + - ci + - cd diff --git a/microsite/data/plugins/github-actions.yaml b/microsite/data/plugins/github-actions.yaml new file mode 100644 index 0000000000..062d0fff61 --- /dev/null +++ b/microsite/data/plugins/github-actions.yaml @@ -0,0 +1,13 @@ +--- +title: GitHub Actions +author: Spotify +authorUrl: https://github.com/spotify +category: CI +description: GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. +documentation: https://github.com/spotify/backstage/tree/master/plugins/github-actions +iconUrl: https://avatars2.githubusercontent.com/u/44036562?s=400&v=4 +npmPackageName: '@backstage/plugin-github-actions' +tags: + - ci + - cd + - github diff --git a/microsite/data/plugins/github-pull-requests.yaml b/microsite/data/plugins/github-pull-requests.yaml new file mode 100644 index 0000000000..917198a221 --- /dev/null +++ b/microsite/data/plugins/github-pull-requests.yaml @@ -0,0 +1,10 @@ +--- +title: GitHub Pull Requests +author: roadie.io +authorUrl: https://roadie.io/ +category: CI +description: View GitHub pull requests for your service in Backstage. +documentation: https://roadie.io/backstage/plugins/github-pull-requests +iconUrl: https://roadie.io/static/7f13bb8d861d8dedc5112fb939d215f9/351f2/GitHub-Mark-Light-120px-plus.png +npmPackageName: '@roadiehq/backstage-plugin-github-pull-requests' + diff --git a/microsite/data/plugins/gitops-cluster.yaml b/microsite/data/plugins/gitops-cluster.yaml new file mode 100644 index 0000000000..6f8ab6b097 --- /dev/null +++ b/microsite/data/plugins/gitops-cluster.yaml @@ -0,0 +1,14 @@ +--- +title: GitOps Clusters +author: Weaveworks +authorUrl: https://www.weave.works/ +category: Kubernetes +description: Create GitOps-managed Kubernetes clusters. Currently, it supports provisioning EKS clusters on GitHub via GitHub Actions. +documentation: https://github.com/spotify/backstage/tree/master/plugins/gitops-profiles +iconUrl: https://res-5.cloudinary.com/crunchbase-production/image/upload/c_lpad,h_256,w_256,f_auto,q_auto:eco/v1462316670/i9d3delzvx1erzjhmcws.png +npmPackageName: '@backstage/plugin-gitops-profiles' +tags: + - kubernetes + - gitops + - github + - eks diff --git a/microsite/data/plugins/graphiql.yaml b/microsite/data/plugins/graphiql.yaml new file mode 100644 index 0000000000..6c95d1b175 --- /dev/null +++ b/microsite/data/plugins/graphiql.yaml @@ -0,0 +1,14 @@ +--- +title: GraphiQL +author: Spotify +authorUrl: https://github.com/spotify +category: Debugging +description: Integrates GraphiQL as a tool to browse GraphiQL endpoints inside Backstage. +documentation: https://github.com/spotify/backstage/tree/master/plugins/lighthouse +iconUrl: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1024px-GraphQL_Logo.svg.png +npmPackageName: '@backstage/plugin-graphiql' +tags: + - graphql + - github + - gitlab + - api diff --git a/microsite/data/plugins/jenkins.yaml b/microsite/data/plugins/jenkins.yaml new file mode 100644 index 0000000000..78b3616595 --- /dev/null +++ b/microsite/data/plugins/jenkins.yaml @@ -0,0 +1,12 @@ +--- +title: Jenkins +author: '@timja' +authorUrl: https://github.com/timja +category: CI +description: Jenkins offers a simple way to set up a continuous integration and continuous delivery environment. +documentation: https://github.com/spotify/backstage/tree/master/plugins/jenkins +iconUrl: https://img.icons8.com/color/1600/jenkins.png +npmPackageName: '@backstage/plugin-jenkins' +tags: + - ci + - cd diff --git a/microsite/data/plugins/lighthouse.yaml b/microsite/data/plugins/lighthouse.yaml new file mode 100644 index 0000000000..0819455ed2 --- /dev/null +++ b/microsite/data/plugins/lighthouse.yaml @@ -0,0 +1,14 @@ +--- +title: Lighthouse +author: Spotify +authorUrl: https://github.com/spotify +category: Accessibility +description: Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website. +documentation: https://github.com/spotify/backstage/tree/master/plugins/lighthouse +iconUrl: https://seeklogo.com/images/G/google-lighthouse-logo-1C7FA08580-seeklogo.com.png +npmPackageName: '@backstage/plugin-lighthouse' +tags: + - web + - seo + - accessibility + - performance diff --git a/microsite/data/plugins/new-relic.yaml b/microsite/data/plugins/new-relic.yaml new file mode 100644 index 0000000000..e3ddf18652 --- /dev/null +++ b/microsite/data/plugins/new-relic.yaml @@ -0,0 +1,14 @@ +--- +title: New Relic +author: '@timwheelercom' +authorUrl: https://github.com/timwheelercom +category: Monitoring +description: Observability platform built to help engineers create and monitor their software. +documentation: https://github.com/spotify/backstage/tree/master/plugins/newrelic +iconUrl: https://www.mulesoft.com/sites/default/files/2018-10/New_relic.png +npmPackageName: '@backstage/plugin-newrelic' +tags: + - performance + - monitoring + - errors + - alerting diff --git a/microsite/data/plugins/rollbar.yaml b/microsite/data/plugins/rollbar.yaml new file mode 100644 index 0000000000..0118fab391 --- /dev/null +++ b/microsite/data/plugins/rollbar.yaml @@ -0,0 +1,10 @@ +--- +title: Rollbar +author: '@andrewthauer' +authorUrl: https://github.com/andrewthauer +category: Monitoring +description: View Rollbar errors for your services in Backstage. +documentation: https://github.com/spotify/backstage/tree/master/plugins/rollbar +iconUrl: https://rollbar.com/assets/media/rollbar-mark-color.png +npmPackageName: '@backstage/plugin-rollbar' + diff --git a/microsite/data/plugins/sentry.yaml b/microsite/data/plugins/sentry.yaml new file mode 100644 index 0000000000..8da488f727 --- /dev/null +++ b/microsite/data/plugins/sentry.yaml @@ -0,0 +1,9 @@ +--- +title: Sentry +author: Spotify +authorUrl: https://github.com/spotify +category: Monitoring +description: View Sentry issues in Backstage. +documentation: https://github.com/spotify/backstage/tree/master/plugins/sentry +iconUrl: https://sentry-brand.storage.googleapis.com/sentry-glyph-white.png +npmPackageName: '@backstage/plugin-sentry' diff --git a/microsite/data/plugins/tech-radar.yaml b/microsite/data/plugins/tech-radar.yaml new file mode 100644 index 0000000000..b1d4f5cc17 --- /dev/null +++ b/microsite/data/plugins/tech-radar.yaml @@ -0,0 +1,9 @@ +--- +title: Tech Radar +author: Spotify +authorUrl: https://github.com/spotify +category: Discovery +description: Visualize the your company's official guidelines of different areas of software development. +documentation: https://github.com/spotify/backstage/tree/master/plugins/tech-radar +iconUrl: https://www.materialui.co/materialIcons/action/track_changes_white_192x192.png +npmPackageName: '@backstage/plugin-tech-radar' diff --git a/microsite/data/plugins/travis-ci.yaml b/microsite/data/plugins/travis-ci.yaml new file mode 100644 index 0000000000..48c0a4cb86 --- /dev/null +++ b/microsite/data/plugins/travis-ci.yaml @@ -0,0 +1,10 @@ +--- +title: Travis CI +author: roadie.io +authorUrl: https://roadie.io/ +category: CI +description: View Travis CI builds for your service in Backstage. +documentation: https://roadie.io/backstage/plugins/travis-ci +iconUrl: https://roadie.io/static/af2941eaf0af675facb281d566f42e14/45f2b/travis-ci-mascot-200x200.png +npmPackageName: '@roadiehq/backstage-plugin-travis-ci' + diff --git a/microsite/package.json b/microsite/package.json new file mode 100644 index 0000000000..ee3c7f8dc2 --- /dev/null +++ b/microsite/package.json @@ -0,0 +1,19 @@ +{ + "version": "0.1.1-alpha.21", + "name": "backstage-microsite", + "license": "Apache-2.0", + "private": true, + "scripts": { + "examples": "docusaurus-examples", + "start": "docusaurus-start", + "build": "docusaurus-build", + "publish-gh-pages": "docusaurus-publish", + "write-translations": "docusaurus-write-translations", + "version": "docusaurus-version", + "rename-version": "docusaurus-rename-version" + }, + "devDependencies": { + "docusaurus": "^2.0.0-alpha.61", + "js-yaml": "^3.14.0" + } +} diff --git a/microsite/pages/en/demos.js b/microsite/pages/en/demos.js new file mode 100644 index 0000000000..9f9dcec819 --- /dev/null +++ b/microsite/pages/en/demos.js @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const Components = require(`${process.cwd()}/core/Components.js`); +const Block = Components.Block; + +const Background = props => { + const { config: siteConfig } = props; + const { baseUrl } = siteConfig; + return ( +
+ + + + See us in action. + + To illustrate the potential of Backstage, we’re showing you{' '} + + how we use it here at Spotify + + . The videos below feature the custom plugins and templates that + we built for our internal version of Backstage. You can use + Backstage to build the developer portal you want — integrating the + tools that you use in your own infrastructure ecosystem. (Or get + started by building an open source plugin for Backstage that + everyone can use, like our{' '} + + Lighthouse Plugin + + .) + + + + + + + + + + + + Introduction to Backstage + + Backstage is an open source platform for building developer + portals. We’ve been using our homegrown version at Spotify for + years — so it’s already packed with features. (We have over 120 + internal plugins, built by 60 different teams.) In this live demo + recording, Stefan Ålund, product manager for Backstage, tells the + origin story of Backstage and gives you a tour of how we use it + here at Spotify. + + + Watch now + + + + + + + + + + + + Manage your tech health + + Instead of manually updating a spreadsheet, what if you had a + beautiful dashboard that could give you an instant, interactive + picture of your entire org’s tech stack? That’s how we do it at + Spotify. With our Tech Insights plugin for Backstage, anyone at + Spotify can see which version of which software anyone else at + Spotify is using — and a whole a lot more. From managing + migrations to fighting tech entropy, Backstage makes managing our + tech health actually kind of pleasant. + + + + Watch now + + + + + + + + + + + + Create a microservice + + You’re a Spotify engineer about to build a new microservice (or + any component) using Spring Boot. Where do you start? Search for a + quick start guide online? Create an empty repo on GitHub? Copy and + paste an old project? Nope. Just go to Backstage, and you’ll be up + and running in two minutes — with a “Hello World” app, CI, and + documentation all automatically set up and configured in a + standardized way. + + + + Watch now + + + + + + + + + + + + Search all your services + + All of Spotify’s services are automatically indexed in Backstage. + So our engineers can stop playing detective — no more spamming + Slack channels asking if anyone knows who owns a particular + service and where you can find its API, only to discover that the + owner went on sabbatical three months ago and you have to hunt + them down on a mountain in Tibet where they’re on a 12-day silent + meditation retreat. At Spotify, anyone can always find anyone + else’s service, inspect its APIs, and contact its current owner — + all with one search. + + + Watch now + + + + + + + + + + + + Manage data pipelines + + We manage a lot of data pipelines (also known as workflows) here + at Spotify. So, of course, we made a great workflows plugin for + our version of Backstage. All our workflow tools — including a + scheduler, log inspector, data lineage graph, and configurable + alerts — are integrated into one simple interface. + + + Watch now + + + + + + + +
+ ); +}; + +module.exports = Background; diff --git a/microsite/pages/en/docs.js b/microsite/pages/en/docs.js new file mode 100644 index 0000000000..a8bda18f88 --- /dev/null +++ b/microsite/pages/en/docs.js @@ -0,0 +1,12 @@ +const React = require('react'); +const Redirect = require('../../core/Redirect.js'); + +const siteConfig = require(process.cwd() + '/siteConfig.js'); + +function Docs() { + return ( + + ); +} + +module.exports = Docs; diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js new file mode 100644 index 0000000000..7a58ede25f --- /dev/null +++ b/microsite/pages/en/index.js @@ -0,0 +1,480 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const Components = require(`${process.cwd()}/core/Components.js`); +const Block = Components.Block; +const ActionBlock = Components.ActionBlock; +const Breakpoint = Components.Breakpoint; +const BulletLine = Components.BulletLine; + +class Index extends React.Component { + render() { + const { config: siteConfig } = this.props; + const { baseUrl } = siteConfig; + + return ( +
+ + + + + An open platform for building developer portals + + + Powered by a centralized service catalog, Backstage restores + order to your infrastructure. So your product teams can ship + high-quality code quickly — without compromising autonomy. + + + GitHub + + + + + + + + + + + + + + The Speed Paradox + + At Spotify, we’ve always believed in the speed and ingenuity + that comes from having autonomous development teams. But as we + learned firsthand, the faster you grow, the more fragmented and + complex your software ecosystem becomes. And then everything + slows down again. + + + + + The Standards Paradox + + By centralizing services and standardizing your tooling, + Backstage streamlines your development environment from end to + end. Instead of restricting autonomy, standardization frees your + engineers from infrastructure complexity. So you can return to + building and scaling, quickly and safely. + + + + + + + + + {' '} + + + Backstage Service Catalog{' '} + + (alpha) + + + + Build an ecosystem, not a wilderness + + + + + + } + /> + + + + Manage all your software, all in one place{' '} + + + Backstage makes it easy for one team to manage 10 services — and + makes it possible for your company to manage thousands of them + + + + + A uniform overview + + Every team can see all the services they own and related + resources (deployments, data pipelines, pull request status, + etc.) + + + + + + Metadata on tap + + All that information can be shared with plugins inside Backstage + to enable other management features, like resource monitoring + and testing + + + + + + Not just services + + Libraries, websites, ML models — you name it, Backstage knows + all about it, including who owns it, dependencies, and more + + + + + + + Discoverability & accountability + + + No more orphan software hiding in the dark corners of your tech + stack + + + + + + + + + Learn more about the service catalog + + + Read + + + + + + + + + Backstage Software Templates{' '} + + (alpha) + + + Standards can set you free + + + + + } + /> + + + + Like automated getting started guides + + + Using templates, engineers can spin up a new microservice with + your organization’s best practices built-in, right from the + start + + + + + + Push-button deployment + + Click a button to create a Spring Boot project with your repo + automatically configured on GitHub and your CI already running + the first build + + + + + + Built to your standards + + Go instead of Java? CircleCI instead of Jenkins? Serverless + instead of Kubernetes? GCP instead of AWS? Customize your + recipes with your best practices baked-in + + + + + + + Golden Paths pave the way + + + When the right way is also the easiest way, engineers get up and + running faster — and more safely + + + + + + } + /> + + + + + + Build your own software templates + + + Contribute + + + + + + + + + + Backstage TechDocs (Coming Soon) + + Docs like code + + + + + + + } + /> + + + Free documentation + + Whenever you use a Backstage Software Template, your project + automatically gets a TechDocs site, for free + + + + + + Easy to write + + With our docs-like-code approach, engineers write their + documentation in Markdown files right alongside their code + + + + + + Easy to maintain + + Updating code? Update your documentation while you’re there — + with docs and code in the same place, it becomes a natural part + of your workstream + + + + + + Easy to find and use + + Since all your documentation is in Backstage, finding any + TechDoc is just a search query away + + + + + + + } + /> + + + + + Subscribe to our newsletter + + TechDocs is our most used feature at Spotify. Be the first to know + when{' '} + + the open source version + {' '} + ships. + + + Subscribe + + + + + + + + + Customize Backstage with plugins + + An app store for your infrastructure + + + + + + } + /> + + + Add functionality + + Want scalable website testing? Add the{' '} + + Lighthouse + {' '} + plugin. Wondering about recommended frameworks? Add the{' '} + + Tech Radar + {' '} + plugin.{' '} + + + + + + BYO Plugins + + If you don’t see the plugin you need, it’s simple to build your + own + + + + + + + Integrate your own custom tooling + + + Building internal plugins lets you tailor your version of + Backstage to be a perfect fit for your infrastructure + + + + + + + Share with the community + + + Building open source plugins contributes + to the entire Backstage ecosystem, which benefits everyone + + + + } + /> + + + + + Build a plugin + + Contribute + + +
+ ); + } +} + +module.exports = Index; diff --git a/microsite/pages/en/plugins.js b/microsite/pages/en/plugins.js new file mode 100644 index 0000000000..0ef8791970 --- /dev/null +++ b/microsite/pages/en/plugins.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +const fs = require('fs'); +const yaml = require('js-yaml'); +const React = require('react'); +const Components = require(`${process.cwd()}/core/Components.js`); +const { + Block: { Container }, + BulletLine, +} = Components; + +const pluginsDirectory = require('path').join(process.cwd(), 'data/plugins'); +const pluginMetadata = fs + .readdirSync(pluginsDirectory) + .map(file => + yaml.safeLoad(fs.readFileSync(`./data/plugins/${file}`, 'utf8')), + ); +const truncate = text => + text.length > 170 ? text.substr(0, 170) + '...' : text; + +const addPluginDocsLink = '/docs/plugins/add-to-marketplace'; +const defaultIconUrl = 'img/logo-gradient-on-dark.svg'; + +const Plugins = () => ( +
+
+
+

Plugin marketplace

+

+ Open source plugins that you can add to your Backstage deployment. + Learn how to build a plugin. +

+ + + Add to marketplace + + +
+ + + {pluginMetadata.map( + ({ + iconUrl, + title, + description, + author, + authorUrl, + documentation, + category, + }) => ( +
+
+ {title} +

{title}

+

+ by {author} +

+ {category} +
+
+

{truncate(description)}

+
+ + + + Explore + + + +
+ ), + )} +
+
+

+ Do you have an existing plugin that you want to add to the + Marketplace? +

+

+ + Add to marketplace + +

+
+ +

+ See what plugins are already{' '} + + in progress + {' '} + and 👍. Missing a plugin for your favorite tool? Please{' '} + + suggest + {' '} + a new one. +

+
+
+
+
+
+); + +module.exports = Plugins; diff --git a/microsite/sidebars.json b/microsite/sidebars.json new file mode 100644 index 0000000000..f5741bd355 --- /dev/null +++ b/microsite/sidebars.json @@ -0,0 +1,152 @@ +{ + "docs": { + "Overview": [ + "overview/what-is-backstage", + "overview/architecture-overview", + "overview/architecture-terminology", + "overview/roadmap", + "overview/vision", + "overview/background", + "overview/adopting" + ], + "Getting Started": [ + "getting-started/index", + "getting-started/installation", + "getting-started/development-environment", + "getting-started/create-an-app", + { + "type": "subcategory", + "label": "App configuration", + "ids": [ + "getting-started/configure-app-with-plugins", + "getting-started/app-custom-theme" + ] + }, + { + "type": "subcategory", + "label": "Deployment", + "ids": [ + "getting-started/deployment-k8s", + "getting-started/deployment-other" + ] + } + ], + "Features": [ + { + "type": "subcategory", + "label": "Software Catalog", + "ids": [ + "features/software-catalog/software-catalog-overview", + "features/software-catalog/system-model", + "features/software-catalog/descriptor-format", + "features/software-catalog/extending-the-model", + "features/software-catalog/external-integrations", + "features/software-catalog/software-catalog-api" + ] + }, + { + "type": "subcategory", + "label": "Software creation templates", + "ids": [ + "features/software-templates/software-templates-index", + "features/software-templates/adding-templates", + "features/software-templates/extending/extending-index", + "features/software-templates/extending/extending-templater", + "features/software-templates/extending/extending-publisher", + "features/software-templates/extending/extending-preparer" + ] + }, + { + "type": "subcategory", + "label": "Docs-like-code", + "ids": [ + "features/techdocs/techdocs-overview", + "features/techdocs/getting-started", + "features/techdocs/concepts", + "features/techdocs/creating-and-publishing", + "features/techdocs/faqs" + ] + } + ], + "Plugins": [ + "plugins/index", + "plugins/existing-plugins", + "plugins/create-a-plugin", + "plugins/plugin-development", + "plugins/structure-of-a-plugin", + "plugins/integrating-plugin-into-service-catalog", + { + "type": "subcategory", + "label": "Backends and APIs", + "ids": [ + "plugins/proxying", + "plugins/backend-plugin", + "plugins/call-existing-api" + ] + }, + { + "type": "subcategory", + "label": "Testing", + "ids": ["plugins/testing"] + }, + { + "type": "subcategory", + "label": "Publishing", + "ids": ["plugins/publishing", "plugins/publish-private", "plugins/add-to-marketplace"] + } + ], + "Configuration": [ + "conf/index", + "conf/reading", + "conf/writing", + "conf/defining" + ], + "Auth and identity": [ + "auth/index", + "auth/add-auth-provider", + "auth/auth-backend", + "auth/oauth", + "auth/glossary", + "auth/auth-backend-classes" + ], + + "Designing for Backstage": [ + "dls/design", + "dls/contributing-to-storybook", + "dls/figma" + ], + "API references": [ + { + "type": "subcategory", + "label": "TypeScript API", + "ids": [ + "api/utility-apis", + "reference/utility-apis/README", + "reference/createPlugin", + "reference/createPlugin-feature-flags", + "reference/createPlugin-router" + ] + }, + { + "type": "subcategory", + "label": "Backend APIs", + "ids": ["api/backend"] + } + ], + "Tutorials": ["tutorials/journey"], + "Architecture Decision Records (ADRs)": [ + "architecture-decisions/adrs-overview", + "architecture-decisions/adrs-adr001", + "architecture-decisions/adrs-adr002", + "architecture-decisions/adrs-adr003", + "architecture-decisions/adrs-adr004", + "architecture-decisions/adrs-adr005", + "architecture-decisions/adrs-adr006", + "architecture-decisions/adrs-adr007", + "architecture-decisions/adrs-adr008" + ], + "Contribute": ["../CONTRIBUTING"], + "Support": ["overview/support"], + "FAQ": ["FAQ"] + } +} diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js new file mode 100644 index 0000000000..cc5ad89e09 --- /dev/null +++ b/microsite/siteConfig.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// See https://docusaurus.io/docs/site-config for all the possible +// site configuration options. + +// List of projects/orgs using your project for the users page. +const users = []; + +const siteConfig = { + title: 'Backstage', // Title for your website. + tagline: 'An open platform for building developer portals', + url: 'https://backstage.io', // Your website URL + cname: 'backstage.io', + baseUrl: '/', // Base URL for your project */ + editUrl: 'https://github.com/spotify/backstage/edit/master/docs/', + + // Used for publishing and more + projectName: 'backstage', + organizationName: 'Spotify', + fossWebsite: 'https://spotify.github.io/', + + // Google Analytics + gaTrackingId: 'UA-48912878-10', + + // For no header links in the top nav bar -> headerLinks: [], + headerLinks: [ + { + href: 'https://github.com/spotify/backstage', + label: 'GitHub', + }, + { + doc: 'overview/what-is-backstage', + href: '/docs', + label: 'Docs', + }, + { + page: 'plugins', + label: 'Plugins', + }, + { + page: 'blog', + blog: true, + label: 'Blog', + }, + { + page: 'demos', + label: 'Demos', + }, + { + href: 'https://mailchi.mp/spotify/backstage-community', + label: 'Newsletter', + }, + ], + + /* path to images for header/footer */ + // headerIcon: "img/android-chrome-192x192.png", + footerIcon: 'img/android-chrome-192x192.png', + favicon: 'img/favicon.svg', + + /* Colors for website */ + colors: { + primaryColor: '#36BAA2', + secondaryColor: '#121212', + textColor: '#FFFFFF', + navigatorTitleTextColor: '#9e9e9e', + navigatorItemTextColor: '#616161', + }, + + /* Colors for syntax highlighting */ + highlight: { + theme: 'dark', + }, + + // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. + copyright: `Copyright © ${new Date().getFullYear()} Spotify AB`, + + highlight: { + // Highlight.js theme to use for syntax highlighting in code blocks. + theme: 'monokai', + }, + + // Add custom scripts here that would be placed in + + + `); +}; + +export const ensuresXRequestedWith = (req: express.Request) => { + const requiredHeader = req.header('X-Requested-With'); + + if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { + return false; + } + return true; +}; diff --git a/plugins/auth-backend/src/lib/flow/index.ts b/plugins/auth-backend/src/lib/flow/index.ts new file mode 100644 index 0000000000..a5f2f7a3ac --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts new file mode 100644 index 0000000000..98bb551c2c --- /dev/null +++ b/plugins/auth-backend/src/lib/flow/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AuthResponse } from '../../providers/types'; + +/** + * Payload sent as a post message after the auth request is complete. + * If successful then has a valid payload with Auth information else contains an error. + */ +export type WebMessageResponse = + | { + type: 'authorization_response'; + response: AuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts similarity index 55% rename from plugins/auth-backend/src/lib/OAuthProvider.test.ts rename to plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 0778b875d0..d2b31213f2 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -15,16 +15,9 @@ */ import express from 'express'; -import { - ensuresXRequestedWith, - postMessageResponse, - THOUSAND_DAYS_MS, - TEN_MINUTES_MS, - verifyNonce, - encodeState, - OAuthProvider, -} from './OAuthProvider'; -import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types'; +import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; +import { encodeState } from './helpers'; +import { OAuthHandlers } from './types'; const mockResponseData = { providerInfo: { @@ -41,149 +34,8 @@ const mockResponseData = { }, }; -describe('OAuthProvider Utils', () => { - describe('verifyNonce', () => { - it('should throw error if cookie nonce missing', () => { - const state = { nonce: 'NONCE', env: 'development' }; - const mockRequest = ({ - cookies: {}, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Auth response is missing cookie nonce'); - }); - - it('should throw error if state nonce missing', () => { - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCE', - }, - query: {}, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Invalid state passed via request'); - }); - - it('should throw error if nonce mismatch', () => { - const state = { nonce: 'NONCEB', env: 'development' }; - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCEA', - }, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).toThrowError('Invalid nonce'); - }); - - it('should not throw any error if nonce matches', () => { - const state = { nonce: 'NONCE', env: 'development' }; - const mockRequest = ({ - cookies: { - 'providera-nonce': 'NONCE', - }, - query: { - state: encodeState(state), - }, - } as unknown) as express.Request; - expect(() => { - verifyNonce(mockRequest, 'providera'); - }).not.toThrow(); - }); - }); - - describe('postMessageResponse', () => { - const appOrigin = 'http://localhost:3000'; - it('should post a message back with payload success', () => { - const mockResponse = ({ - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - response: { - providerInfo: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', - }, - profile: { - email: 'foo@bar.com', - }, - backstageIdentity: { - id: 'a', - idToken: 'a.b.c', - }, - }, - }; - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toBeCalledTimes(3); - expect(mockResponse.end).toBeCalledTimes(1); - expect(mockResponse.end).toBeCalledWith( - expect.stringContaining(base64Data), - ); - }); - - it('should post a message back with payload error', () => { - const mockResponse = ({ - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - - const data: WebMessageResponse = { - type: 'authorization_response', - error: new Error('Unknown error occured'), - }; - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - postMessageResponse(mockResponse, appOrigin, data); - expect(mockResponse.setHeader).toBeCalledTimes(3); - expect(mockResponse.end).toBeCalledTimes(1); - expect(mockResponse.end).toBeCalledWith( - expect.stringContaining(base64Data), - ); - }); - }); - - describe('ensuresXRequestedWith', () => { - it('should return false if no header present', () => { - const mockRequest = ({ - header: () => jest.fn(), - } as unknown) as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(false); - }); - - it('should return false if header present with incorrect value', () => { - const mockRequest = ({ - header: () => 'INVALID', - } as unknown) as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(false); - }); - - it('should return true if header present with correct value', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; - expect(ensuresXRequestedWith(mockRequest)).toBe(true); - }); - }); -}); - -describe('OAuthProvider', () => { - class MyAuthProvider implements OAuthProviderHandlers { +describe('OAuthAdapter', () => { + class MyAuthProvider implements OAuthHandlers { async start() { return { url: '/url', @@ -205,8 +57,9 @@ describe('OAuthProvider', () => { providerId: 'test-provider', secure: false, disableRefresh: true, - baseUrl: 'http://localhost:7000/auth', appOrigin: 'http://localhost:3000', + cookieDomain: 'localhost', + cookiePath: '/auth/test-provider', tokenIssuer: { issueToken: async () => 'my-id-token', listPublicKeys: async () => ({ keys: [] }), @@ -214,7 +67,7 @@ describe('OAuthProvider', () => { }; it('sets the correct headers in start', async () => { - const oauthProvider = new OAuthProvider( + const oauthProvider = new OAuthAdapter( providerInstance, oAuthProviderOptions, ); @@ -249,7 +102,7 @@ describe('OAuthProvider', () => { }); it('sets the refresh cookie if refresh is enabled', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -283,7 +136,7 @@ describe('OAuthProvider', () => { }); it('does not set the refresh cookie if refresh is disabled', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, }); @@ -308,7 +161,7 @@ describe('OAuthProvider', () => { }); it('removes refresh cookie when logging out', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -333,7 +186,7 @@ describe('OAuthProvider', () => { it('gets new access-token when refreshing', async () => { oAuthProviderOptions.disableRefresh = false; - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, }); @@ -362,7 +215,7 @@ describe('OAuthProvider', () => { }); it('handles refresh without capabilities', async () => { - const oauthProvider = new OAuthProvider(providerInstance, { + const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, }); diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts similarity index 62% rename from plugins/auth-backend/src/lib/OAuthProvider.ts rename to plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index f2b8c8e09c..0f092ae780 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -19,13 +19,14 @@ import crypto from 'crypto'; import { URL } from 'url'; import { AuthProviderRouteHandlers, - OAuthProviderHandlers, - WebMessageResponse, BackstageIdentity, - OAuthState, -} from '../providers/types'; + AuthProviderConfig, +} from '../../providers/types'; import { InputError } from '@backstage/backend-common'; -import { TokenIssuer } from '../identity'; +import { TokenIssuer } from '../../identity'; +import { verifyNonce, encodeState } from './helpers'; +import { postMessageResponse, ensuresXRequestedWith } from '../flow'; +import { OAuthHandlers } from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -35,102 +36,38 @@ export type Options = { secure: boolean; disableRefresh?: boolean; persistScopes?: boolean; - baseUrl: string; + cookieDomain: string; + cookiePath: string; appOrigin: string; tokenIssuer: TokenIssuer; }; -const readState = (stateString: string): OAuthState => { - const state = Object.fromEntries( - new URLSearchParams(decodeURIComponent(stateString)), - ); - if ( - !state.nonce || - !state.env || - state.nonce?.length === 0 || - state.env?.length === 0 - ) { - throw Error(`Invalid state passed via request`); +export class OAuthAdapter implements AuthProviderRouteHandlers { + static fromConfig( + config: AuthProviderConfig, + handlers: OAuthHandlers, + options: Pick< + Options, + 'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer' + >, + ): OAuthAdapter { + const { origin: appOrigin } = new URL(config.appUrl); + const secure = config.baseUrl.startsWith('https://'); + const url = new URL(config.baseUrl); + const cookiePath = `${url.pathname}/${options.providerId}`; + return new OAuthAdapter(handlers, { + ...options, + appOrigin, + cookieDomain: url.hostname, + cookiePath, + secure, + }); } - return { - nonce: state.nonce, - env: state.env, - }; -}; - -export const encodeState = (state: OAuthState): string => { - const searchParams = new URLSearchParams(); - searchParams.append('nonce', state.nonce); - searchParams.append('env', state.env); - - return encodeURIComponent(searchParams.toString()); -}; - -export const verifyNonce = (req: express.Request, providerId: string) => { - const cookieNonce = req.cookies[`${providerId}-nonce`]; - const state: OAuthState = readState(req.query.state?.toString() ?? ''); - const stateNonce = state.nonce; - - if (!cookieNonce) { - throw new Error('Auth response is missing cookie nonce'); - } - if (stateNonce.length === 0) { - throw new Error('Auth response is missing state nonce'); - } - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const postMessageResponse = ( - res: express.Response, - appOrigin: string, - response: WebMessageResponse, -) => { - const jsonData = JSON.stringify(response); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - res.setHeader('Content-Type', 'text/html'); - res.setHeader('X-Frame-Options', 'sameorigin'); - - // TODO: Make target app origin configurable globally - const script = ` - (window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}') - window.close() - `; - const hash = crypto.createHash('sha256').update(script).digest('base64'); - res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); - - res.end(` - - - - - - `); -}; - -export const ensuresXRequestedWith = (req: express.Request) => { - const requiredHeader = req.header('X-Requested-With'); - - if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { - return false; - } - return true; -}; - -export class OAuthProvider implements AuthProviderRouteHandlers { - private readonly domain: string; - private readonly basePath: string; constructor( - private readonly providerHandlers: OAuthProviderHandlers, + private readonly handlers: OAuthHandlers, private readonly options: Options, - ) { - const url = new URL(options.baseUrl); - this.domain = url.hostname; - this.basePath = url.pathname; - } + ) {} async start(req: express.Request, res: express.Response): Promise { // retrieve scopes from request @@ -157,10 +94,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { state: stateParameter, }; - const { url, status } = await this.providerHandlers.start( - req, - queryParameters, - ); + const { url, status } = await this.handlers.start(req, queryParameters); res.statusCode = status || 302; res.setHeader('Location', url); @@ -176,9 +110,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); - const { response, refreshToken } = await this.providerHandlers.handler( - req, - ); + const { response, refreshToken } = await this.handlers.handler(req); if (this.options.persistScopes) { const grantedScopes = this.getScopesFromCookie( @@ -235,7 +167,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { return; } - if (!this.providerHandlers.refresh || this.options.disableRefresh) { + if (!this.handlers.refresh || this.options.disableRefresh) { res.send( `Refresh token not supported for provider: ${this.options.providerId}`, ); @@ -254,29 +186,23 @@ export class OAuthProvider implements AuthProviderRouteHandlers { const scope = req.query.scope?.toString() ?? ''; // get new access_token - const response = await this.providerHandlers.refresh(refreshToken, scope); + const response = await this.handlers.refresh(refreshToken, scope); await this.populateIdentity(response.backstageIdentity); + if ( + response.providerInfo.refreshToken && + response.providerInfo.refreshToken !== refreshToken + ) { + this.setRefreshTokenCookie(res, response.providerInfo.refreshToken); + } + res.send(response); } catch (error) { res.status(401).send(`${error.message}`); } } - identifyEnv(req: express.Request): string | undefined { - const reqEnv = req.query.env?.toString(); - if (reqEnv) { - return reqEnv; - } - const stateParams = req.query.state?.toString(); - if (!stateParams) { - return undefined; - } - const env = readState(stateParams).env; - return env; - } - /** * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. @@ -298,8 +224,8 @@ export class OAuthProvider implements AuthProviderRouteHandlers { maxAge: TEN_MINUTES_MS, secure: this.options.secure, sameSite: 'lax', - domain: this.domain, - path: `${this.basePath}/${this.options.providerId}/handler`, + domain: this.options.cookieDomain, + path: `${this.options.cookiePath}/handler`, httpOnly: true, }); }; @@ -309,8 +235,8 @@ export class OAuthProvider implements AuthProviderRouteHandlers { maxAge: TEN_MINUTES_MS, secure: this.options.secure, sameSite: 'lax', - domain: this.domain, - path: `${this.basePath}/${this.options.providerId}/handler`, + domain: this.options.cookieDomain, + path: `${this.options.cookiePath}/handler`, httpOnly: true, }); }; @@ -327,8 +253,8 @@ export class OAuthProvider implements AuthProviderRouteHandlers { maxAge: THOUSAND_DAYS_MS, secure: this.options.secure, sameSite: 'lax', - domain: this.domain, - path: `${this.basePath}/${this.options.providerId}`, + domain: this.options.cookieDomain, + path: this.options.cookiePath, httpOnly: true, }); }; @@ -336,10 +262,10 @@ export class OAuthProvider implements AuthProviderRouteHandlers { private removeRefreshTokenCookie = (res: express.Response) => { res.cookie(`${this.options.providerId}-refresh-token`, '', { maxAge: 0, - secure: false, + secure: this.options.secure, sameSite: 'lax', - domain: `${this.domain}`, - path: `${this.basePath}/${this.options.providerId}`, + domain: this.options.cookieDomain, + path: this.options.cookiePath, httpOnly: true, }); }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts new file mode 100644 index 0000000000..d22fc52499 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/backend-common'; +import { readState } from './helpers'; +import { AuthProviderRouteHandlers } from '../../providers/types'; + +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ) { + const envs = config.keys(); + const handlers = new Map(); + + for (const env of envs) { + const envConfig = config.getConfig(env); + const handler = factoryFunc(envConfig); + handlers.set(env, handler); + } + + return new OAuthEnvironmentHandler(handlers); + } + + constructor( + private readonly handlers: Map, + ) {} + + async start(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req, res); + await provider?.start(req, res); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + const provider = this.getProviderForEnv(req, res); + await provider?.frameHandler(req, res); + } + + async refresh(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req, res); + await provider?.refresh?.(req, res); + } + + async logout(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req, res); + await provider?.logout?.(req, res); + } + + private getRequestFromEnv(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + const stateParams = req.query.state?.toString(); + if (!stateParams) { + return undefined; + } + const env = readState(stateParams).env; + return env; + } + + private getProviderForEnv( + req: express.Request, + res: express.Response, + ): AuthProviderRouteHandlers | undefined { + const env: string | undefined = this.getRequestFromEnv(req); + + if (!env) { + throw new InputError(`Must specify 'env' query to select environment`); + } + + if (!this.handlers.has(env)) { + res.status(404).send( + `Missing configuration. +
+
+ For this flow to work you need to supply a valid configuration for the "${env}" environment of provider.`, + ); + return undefined; + } + + return this.handlers.get(env); + } +} diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts new file mode 100644 index 0000000000..fcf56705d2 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { verifyNonce, encodeState } from './helpers'; + +describe('OAuthProvider Utils', () => { + describe('verifyNonce', () => { + it('should throw error if cookie nonce missing', () => { + const state = { nonce: 'NONCE', env: 'development' }; + const mockRequest = ({ + cookies: {}, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Auth response is missing cookie nonce'); + }); + + it('should throw error if state nonce missing', () => { + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCE', + }, + query: {}, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Invalid state passed via request'); + }); + + it('should throw error if nonce mismatch', () => { + const state = { nonce: 'NONCEB', env: 'development' }; + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCEA', + }, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).toThrowError('Invalid nonce'); + }); + + it('should not throw any error if nonce matches', () => { + const state = { nonce: 'NONCE', env: 'development' }; + const mockRequest = ({ + cookies: { + 'providera-nonce': 'NONCE', + }, + query: { + state: encodeState(state), + }, + } as unknown) as express.Request; + expect(() => { + verifyNonce(mockRequest, 'providera'); + }).not.toThrow(); + }); + }); +}); diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts new file mode 100644 index 0000000000..9f250a0285 --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { OAuthState } from './types'; + +export const readState = (stateString: string): OAuthState => { + const state = Object.fromEntries( + new URLSearchParams(decodeURIComponent(stateString)), + ); + if ( + !state.nonce || + !state.env || + state.nonce?.length === 0 || + state.env?.length === 0 + ) { + throw Error(`Invalid state passed via request`); + } + return { + nonce: state.nonce, + env: state.env, + }; +}; + +export const encodeState = (state: OAuthState): string => { + const searchParams = new URLSearchParams(); + searchParams.append('nonce', state.nonce); + searchParams.append('env', state.env); + + return encodeURIComponent(searchParams.toString()); +}; + +export const verifyNonce = (req: express.Request, providerId: string) => { + const cookieNonce = req.cookies[`${providerId}-nonce`]; + const state: OAuthState = readState(req.query.state?.toString() ?? ''); + const stateNonce = state.nonce; + + if (!cookieNonce) { + throw new Error('Auth response is missing cookie nonce'); + } + if (stateNonce.length === 0) { + throw new Error('Auth response is missing state nonce'); + } + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts new file mode 100644 index 0000000000..05c8bd9d3d --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; +export { OAuthAdapter } from './OAuthAdapter'; +export type { + OAuthHandlers, + OAuthProviderInfo, + OAuthProviderOptions, + OAuthResponse, + OAuthState, +} from './types'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts new file mode 100644 index 0000000000..a854326bed --- /dev/null +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { AuthResponse, RedirectInfo } from '../../providers/types'; + +/** + * Common options for passport.js-based OAuth providers + */ +export type OAuthProviderOptions = { + /** + * Client ID of the auth provider. + */ + clientId: string; + /** + * Client Secret of the auth provider. + */ + clientSecret: string; + /** + * Callback URL to be passed to the auth provider to redirect to after the user signs in. + */ + callbackUrl: string; +}; + +export type OAuthResponse = AuthResponse; + +export type OAuthProviderInfo = { + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * (Optional) Id token issued for the signed in user. + */ + idToken?: string; + /** + * Expiry of the access token in seconds. + */ + expiresInSeconds?: number; + /** + * Scopes granted for the access token. + */ + scope: string; + /** + * A refresh token issued for the signed in user + */ + refreshToken?: string; +}; + +export type OAuthState = { + /* A type for the serialized value in the `state` parameter of the OAuth authorization flow + */ + nonce: string; + env: string; +}; + +/** + * Any OAuth provider needs to implement this interface which has provider specific + * handlers for different methods to perform authentication, get access tokens, + * refresh tokens and perform sign out. + */ +export interface OAuthHandlers { + /** + * This method initiates a sign in request with an auth provider. + * @param {express.Request} req + * @param options + */ + start( + req: express.Request, + options: Record, + ): Promise; + + /** + * Handles the redirect from the auth provider when the user has signed in. + * @param {express.Request} req + */ + handler( + req: express.Request, + ): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; + + /** + * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. + * @param {string} refreshToken + * @param {string} scope + */ + refresh?( + refreshToken: string, + scope: string, + ): Promise>; + + /** + * (Optional) Sign out of the auth provider. + */ + logout?(): Promise; +} diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts similarity index 100% rename from plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts rename to plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts similarity index 88% rename from plugins/auth-backend/src/lib/PassportStrategyHelper.ts rename to plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 4cc0de4444..7bc34186f4 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -17,12 +17,13 @@ import express from 'express'; import passport from 'passport'; import jwtDecoder from 'jwt-decode'; -import { - RedirectInfo, - RefreshTokenResponse, - ProfileInfo, - ProviderStrategy, -} from '../providers/types'; +import { ProfileInfo, RedirectInfo } from '../../providers/types'; + +export type PassportDoneCallback = ( + err?: Error, + response?: Res, + privateInfo?: Private, +) => void; export const makeProfileInfo = ( profile: passport.Profile, @@ -45,7 +46,6 @@ export const makeProfileInfo = ( if ((!email || !picture) && idToken) { try { const decoded: Record = jwtDecoder(idToken); - if (!email && decoded.email) { email = decoded.email; } @@ -107,6 +107,18 @@ export const executeFrameHandlerStrategy = async ( ); }; +type RefreshTokenResponse = { + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * Optionally, the server can issue a new Refresh Token for the user + */ + refreshToken?: string; + params: any; +}; + export const executeRefreshTokenStrategy = async ( providerStrategy: passport.Strategy, refreshToken: string, @@ -133,7 +145,7 @@ export const executeRefreshTokenStrategy = async ( ( err: Error | null, accessToken: string, - _refreshToken: string, + newRefreshToken: string, params: any, ) => { if (err) { @@ -149,6 +161,7 @@ export const executeRefreshTokenStrategy = async ( resolve({ accessToken, + refreshToken: newRefreshToken, params, }); }, @@ -156,6 +169,10 @@ export const executeRefreshTokenStrategy = async ( }); }; +type ProviderStrategy = { + userProfile(accessToken: string, callback: Function): void; +}; + export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, diff --git a/plugins/auth-backend/src/lib/passport/index.ts b/plugins/auth-backend/src/lib/passport/index.ts new file mode 100644 index 0000000000..c307e212fa --- /dev/null +++ b/plugins/auth-backend/src/lib/passport/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, +} from './PassportStrategyHelper'; +export type { PassportDoneCallback } from './PassportStrategyHelper'; diff --git a/plugins/github-actions/src/components/WorkflowRunDetailsPage/index.ts b/plugins/auth-backend/src/providers/auth0/index.ts similarity index 89% rename from plugins/github-actions/src/components/WorkflowRunDetailsPage/index.ts rename to plugins/auth-backend/src/providers/auth0/index.ts index 443bcacc88..87c9aceaa4 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetailsPage/index.ts +++ b/plugins/auth-backend/src/providers/auth0/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { WorkflowRunDetailsPage } from './WorkflowRunDetailsPage'; +export { createAuth0Provider } from './provider'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts new file mode 100644 index 0000000000..3b7817ffe9 --- /dev/null +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import passport from 'passport'; +import Auth0Strategy from './strategy'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, +} from '../../lib/oauth'; +import { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, + PassportDoneCallback, +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; + +type PrivateInfo = { + refreshToken: string; +}; + +export type Auth0AuthProviderOptions = OAuthProviderOptions & { + domain: string; +}; + +export class Auth0AuthProvider implements OAuthHandlers { + private readonly _strategy: Auth0Strategy; + + constructor(options: Auth0AuthProviderOptions) { + this._strategy = new Auth0Strategy( + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + domain: options.domain, + passReqToCallback: false as true, + }, + ( + accessToken: any, + refreshToken: any, + params: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, + ) => { + const profile = makeProfileInfo(rawProfile, params.id_token); + done( + undefined, + { + providerInfo: { + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + profile, + }, + { + refreshToken, + }, + ); + }, + ); + } + + async start( + req: express.Request, + options: Record, + ): Promise { + const providerOptions = { + ...options, + accessType: 'offline', + prompt: 'consent', + }; + return await executeRedirectStrategy(req, this._strategy, providerOptions); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { response, privateInfo } = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, this._strategy); + + return { + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(refreshToken: string, scope: string): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + + const profile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + params.id_token, + ); + + return this.populateIdentity({ + providerInfo: { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + profile, + }); + } + + // Use this function to grab the user profile info from the token + // Then populate the profile with it + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('Profile does not contain a profile'); + } + + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; + } +} + +export const createAuth0Provider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'auth0'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); + }); diff --git a/plugins/auth-backend/src/providers/auth0/strategy.ts b/plugins/auth-backend/src/providers/auth0/strategy.ts new file mode 100644 index 0000000000..6ac06ec4e9 --- /dev/null +++ b/plugins/auth-backend/src/providers/auth0/strategy.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import OAuth2Strategy from 'passport-oauth2'; + +export interface Auth0StrategyOptionsWithRequest { + clientID: string; + clientSecret: string; + callbackURL: string; + domain: string; + passReqToCallback: true; +} + +export default class Auth0Strategy extends OAuth2Strategy { + constructor( + options: Auth0StrategyOptionsWithRequest, + verify: OAuth2Strategy.VerifyFunctionWithRequest, + ) { + const optionsWithURLs = { + ...options, + authorizationURL: `https://${options.domain}/authorize`, + tokenURL: `https://${options.domain}/oauth/token`, + userInfoURL: `https://${options.domain}/userinfo`, + apiUrl: `https://${options.domain}/api`, + }; + super(optionsWithURLs, verify); + } +} diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 3311bfc836..948cc3eb8f 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -23,16 +23,10 @@ import { createGoogleProvider } from './google'; import { createOAuth2Provider } from './oauth2'; import { createOktaProvider } from './okta'; import { createSamlProvider } from './saml'; -import { - AuthProviderConfig, - AuthProviderFactory, - EnvironmentIdentifierFn, -} from './types'; +import { createAuth0Provider } from './auth0'; +import { createMicrosoftProvider } from './microsoft'; +import { AuthProviderConfig, AuthProviderFactory } from './types'; import { Config } from '@backstage/config'; -import { - EnvironmentHandlers, - EnvironmentHandler, -} from '../lib/EnvironmentHandler'; const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, @@ -40,15 +34,17 @@ const factories: { [providerId: string]: AuthProviderFactory } = { gitlab: createGitlabProvider, saml: createSamlProvider, okta: createOktaProvider, + auth0: createAuth0Provider, + microsoft: createMicrosoftProvider, oauth2: createOAuth2Provider, }; export const createAuthProviderRouter = ( providerId: string, globalConfig: AuthProviderConfig, - providerConfig: Config, + config: Config, logger: Logger, - issuer: TokenIssuer, + tokenIssuer: TokenIssuer, ) => { const factory = factories[providerId]; if (!factory) { @@ -56,10 +52,8 @@ export const createAuthProviderRouter = ( } const router = Router(); - const envs = providerConfig.keys(); - const envProviders: EnvironmentHandlers = {}; - let envIdentifier: EnvironmentIdentifierFn | undefined; +<<<<<<< HEAD for (const env of envs) { const envConfig = providerConfig.getConfig(env); console.log(envConfig); @@ -79,6 +73,9 @@ export const createAuthProviderRouter = ( envProviders, envIdentifier, ); +======= + const handler = factory({ globalConfig, config, logger, tokenIssuer }); +>>>>>>> master router.get('/start', handler.start.bind(handler)); router.get('/handler/frame', handler.frameHandler.bind(handler)); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 863cfa9f5a..2205fb6794 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -20,22 +20,25 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthProviderConfig, - RedirectInfo, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, +} from '../../lib/oauth'; import passport from 'passport'; -import { Config } from '@backstage/config'; -export class GithubAuthProvider implements OAuthProviderHandlers { +export type GithubAuthProviderOptions = OAuthProviderOptions & { + tokenUrl?: string; + userProfileUrl?: string; + authorizationUrl?: string; +}; + +export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; static transformPassportProfile(rawProfile: any): passport.Profile { @@ -68,7 +71,7 @@ export class GithubAuthProvider implements OAuthProviderHandlers { idToken: params.id_token, }; - // Github provides an id numeric value (123) + // GitHub provides an id numeric value (123) // as a fallback const id = passportProfile!.id; @@ -87,9 +90,16 @@ export class GithubAuthProvider implements OAuthProviderHandlers { }; } - constructor(options: OAuthProviderOptions) { + constructor(options: GithubAuthProviderOptions) { this._strategy = new GithubStrategy( - { ...options }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + tokenURL: options.tokenUrl, + userProfileURL: options.userProfileUrl, + authorizationURL: options.authorizationUrl, + }, ( accessToken: any, _: any, @@ -124,60 +134,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers { } } -export function createGithubProvider( - { baseUrl }: AuthProviderConfig, - _: string, - envConfig: Config, - logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'github'; - const secure = envConfig.getBoolean('secure'); - const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig.getOptionalString( - 'enterpriseInstanceUrl', - ); - const authorizationURL = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenURL = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileURL = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackURL = `${baseUrl}/${providerId}/handler/frame`; - - const opts = { - clientID, - clientSecret, - authorizationURL, - tokenURL, - userProfileURL, - callbackURL, - }; - - if (!opts.clientID || !opts.clientSecret) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars', - ); - } - - logger.warn( - 'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable', +export const createGithubProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'github'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig.getOptionalString( + 'enterpriseInstanceUrl', ); - return undefined; - } - return new OAuthProvider(new GithubAuthProvider(opts), { - disableRefresh: true, - persistScopes: true, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + persistScopes: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 22d0bd4f0a..97f0b2bd22 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -20,22 +20,23 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthProviderConfig, - RedirectInfo, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, +} from '../../lib/oauth'; import passport from 'passport'; -import { Config } from '@backstage/config'; -export class GitlabAuthProvider implements OAuthProviderHandlers { +export type GitlabAuthProviderOptions = OAuthProviderOptions & { + baseUrl: string; +}; + +export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; static transformPassportProfile(rawProfile: any): passport.Profile { @@ -96,9 +97,14 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { }; } - constructor(options: OAuthProviderOptions) { + constructor(options: GitlabAuthProviderOptions) { this._strategy = new GitlabStrategy( - { ...options }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + baseURL: options.baseUrl, + }, ( accessToken: any, _: any, @@ -131,47 +137,29 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { } } -export function createGitlabProvider( - { baseUrl }: AuthProviderConfig, - _: string, - envConfig: Config, - logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'gitlab'; - const secure = envConfig.getBoolean('secure'); - const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const baseURL = audience || 'https://gitlab.com'; - const callbackURL = `${baseUrl}/${providerId}/handler/frame`; +export const createGitlabProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'gitlab'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const opts = { - clientID, - clientSecret, - callbackURL, - baseURL, - }; + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + }); - if (!opts.clientID || !opts.clientSecret) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars', - ); - } - - logger.warn( - 'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable', - ); - return undefined; - } - return new OAuthProvider(new GitlabAuthProvider(opts), { - disableRefresh: true, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index b8070949f9..9ee2e1d680 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -22,34 +22,36 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, -} from '../../lib/PassportStrategyHelper'; + PassportDoneCallback, +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthProviderHandlers, - RedirectInfo, - AuthProviderConfig, + OAuthAdapter, + OAuthHandlers, OAuthProviderOptions, OAuthResponse, - PassportDoneCallback, -} from '../types'; -import { OAuthProvider } from '../../lib/OAuthProvider'; + OAuthEnvironmentHandler, +} from '../../lib/oauth'; import passport from 'passport'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; }; -export class GoogleAuthProvider implements OAuthProviderHandlers { +export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; constructor(options: OAuthProviderOptions) { // TODO: throw error if env variables not set? this._strategy = new GoogleStrategy( - // We need passReqToCallback set to false to get params, but there's - // no matching type signature for that, so instead behold this beauty - { ...options, passReqToCallback: false as true }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + // We need passReqToCallback set to false to get params, but there's + // no matching type signature for that, so instead behold this beauty + passReqToCallback: false as true, + }, ( accessToken: any, refreshToken: any, @@ -143,44 +145,26 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { } } -export function createGoogleProvider( - { baseUrl }: AuthProviderConfig, - _: string, - envConfig: Config, - logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'google'; - const secure = envConfig.getBoolean('secure'); - const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackURL = `${baseUrl}/${providerId}/handler/frame`; +export const createGoogleProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'google'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const opts = { - clientID, - clientSecret, - callbackURL, - }; + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + }); - if (!opts.clientID || !opts.clientSecret) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars', - ); - } - - logger.warn( - 'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable', - ); - return undefined; - } - return new OAuthProvider(new GoogleAuthProvider(opts), { - disableRefresh: false, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/microsoft/index.ts b/plugins/auth-backend/src/providers/microsoft/index.ts new file mode 100644 index 0000000000..2e4abd2d2c --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createMicrosoftProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts new file mode 100644 index 0000000000..edc5509d84 --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -0,0 +1,234 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import passport from 'passport'; +import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; + +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, + executeFetchUserProfileStrategy, + PassportDoneCallback, +} from '../../lib/passport'; + +import { RedirectInfo, AuthProviderFactory } from '../types'; + +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, +} from '../../lib/oauth'; + +import got from 'got'; + +type PrivateInfo = { + refreshToken: string; +}; + +export type MicrosoftAuthProviderOptions = OAuthProviderOptions & { + authorizationUrl?: string; + tokenUrl?: string; +}; + +export class MicrosoftAuthProvider implements OAuthHandlers { + private readonly _strategy: MicrosoftStrategy; + + static transformAuthResponse( + accessToken: string, + params: any, + rawProfile: any, + photoURL: any, + ): OAuthResponse { + let passportProfile: passport.Profile = rawProfile; + passportProfile = { + ...passportProfile, + photos: [{ value: photoURL }], + }; + + const profile = makeProfileInfo(passportProfile, params.id_token); + const providerInfo = { + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }; + + return { + providerInfo, + profile, + }; + } + + constructor(options: MicrosoftAuthProviderOptions) { + this._strategy = new MicrosoftStrategy( + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + authorizationURL: options.authorizationUrl, + tokenURL: options.tokenUrl, + passReqToCallback: false as true, + }, + ( + accessToken: any, + refreshToken: any, + params: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, + ) => { + this.getUserPhoto(accessToken) + .then(photoURL => { + const authResponse = MicrosoftAuthProvider.transformAuthResponse( + accessToken, + params, + rawProfile, + photoURL, + ); + done(undefined, authResponse, { refreshToken }); + }) + .catch(error => { + throw new Error(`Error processing auth response: ${error}`); + }); + }, + ); + } + + async start( + req: express.Request, + options: Record, + ): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { response, privateInfo } = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, this._strategy); + + return { + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(refreshToken: string, scope: string): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + + const profile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + params.id_token, + ); + const photo = await this.getUserPhoto(accessToken); + if (photo) { + profile.picture = photo; + } + + return this.populateIdentity({ + providerInfo: { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + profile, + }); + } + + private getUserPhoto(accessToken: string): Promise { + return new Promise(resolve => { + got + .get('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', { + encoding: 'binary', + responseType: 'buffer', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .then(photoData => { + const photoURL = `data:image/jpeg;base64,${Buffer.from( + photoData.body, + ).toString('base64')}`; + resolve(photoURL); + }) + .catch(error => { + console.log( + `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, + ); + // User profile photo is optional, ignore errors and resolve undefined + resolve(); + }); + }); + } + + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('Microsoft profile contained no email'); + } + + // Like Google implementation, setting this to local part of email for now + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; + } +} + +export const createMicrosoftProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'microsoft'; + + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantID = envConfig.getString('tenantId'); + + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; + + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + }); diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index c03f7c4371..5a4882fa6f 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -17,36 +17,45 @@ import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, +} from '../../lib/oauth'; import { executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, executeRefreshTokenStrategy, makeProfileInfo, -} from '../../lib/PassportStrategyHelper'; -import { - AuthProviderConfig, - GenericOAuth2ProviderOptions, - OAuthProviderHandlers, - OAuthResponse, PassportDoneCallback, - RedirectInfo, -} from '../types'; -import { Config } from '@backstage/config'; +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { refreshToken: string; }; -export class OAuth2AuthProvider implements OAuthProviderHandlers { +export type OAuth2AuthProviderOptions = OAuthProviderOptions & { + authorizationUrl: string; + tokenUrl: string; +}; + +export class OAuth2AuthProvider implements OAuthHandlers { private readonly _strategy: OAuth2Strategy; - constructor(options: GenericOAuth2ProviderOptions) { + constructor(options: OAuth2AuthProviderOptions) { this._strategy = new OAuth2Strategy( - { ...options, passReqToCallback: false as true }, + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + authorizationURL: options.authorizationUrl, + tokenURL: options.tokenUrl, + passReqToCallback: false as true, + }, ( accessToken: any, refreshToken: any, @@ -55,6 +64,7 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { done: PassportDoneCallback, ) => { const profile = makeProfileInfo(rawProfile, params.id_token); + done( undefined, { @@ -101,11 +111,16 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { } async refresh(refreshToken: string, scope: string): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( + const refreshTokenResponse = await executeRefreshTokenStrategy( this._strategy, refreshToken, scope, ); + const { + accessToken, + params, + refreshToken: updatedRefreshToken, + } = refreshTokenResponse; const profile = await executeFetchUserProfileStrategy( this._strategy, @@ -116,6 +131,7 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { return this.populateIdentity({ providerInfo: { accessToken, + refreshToken: updatedRefreshToken, idToken: params.id_token, expiresInSeconds: params.expires_in, scope: params.scope, @@ -134,60 +150,36 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { if (!profile.email) { throw new Error('Profile does not contain a profile'); } - const id = profile.email.split('@')[0]; return { ...response, backstageIdentity: { id } }; } } -export function createOAuth2Provider( - { baseUrl }: AuthProviderConfig, - _: string, - envConfig: Config, - logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'oauth2'; - const secure = envConfig.getBoolean('secure'); - const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackURL = `${baseUrl}/${providerId}/handler/frame`; - const authorizationURL = envConfig.getString('authorizationURL'); - const tokenURL = envConfig.getString('tokenURL'); +export const createOAuth2Provider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'oauth2'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); - const opts = { - clientID, - clientSecret, - callbackURL, - authorizationURL, - tokenURL, - }; + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); - if ( - !opts.clientID || - !opts.clientSecret || - !opts.authorizationURL || - !opts.tokenURL - ) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars', - ); - } - - logger.warn( - 'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable', - ); - return undefined; - } - return new OAuthProvider(new OAuth2AuthProvider(opts), { - disableRefresh: false, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 058300cef6..0368bd8415 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,7 +14,13 @@ * limitations under the License. */ import express from 'express'; -import { OAuthProvider } from '../../lib/OAuthProvider'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, +} from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; import { @@ -23,25 +29,20 @@ import { executeRefreshTokenStrategy, makeProfileInfo, executeFetchUserProfileStrategy, -} from '../../lib/PassportStrategyHelper'; -import { - OAuthProviderHandlers, - RedirectInfo, - AuthProviderConfig, - OAuthProviderOptions, - OAuthResponse, PassportDoneCallback, -} from '../types'; -import { Logger } from 'winston'; +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; import { StateStore } from 'passport-oauth2'; -import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; }; -export class OktaAuthProvider implements OAuthProviderHandlers { +export type OktaAuthProviderOptions = OAuthProviderOptions & { + audience: string; +}; + +export class OktaAuthProvider implements OAuthHandlers { private readonly _strategy: any; /** @@ -61,11 +62,14 @@ export class OktaAuthProvider implements OAuthProviderHandlers { }, }; - constructor(options: OAuthProviderOptions) { + constructor(options: OktaAuthProviderOptions) { this._strategy = new OktaStrategy( { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + audience: options.audience, passReqToCallback: false as true, - ...options, store: this._store, response_type: 'code', }, @@ -163,46 +167,28 @@ export class OktaAuthProvider implements OAuthProviderHandlers { } } -export function createOktaProvider( - { baseUrl }: AuthProviderConfig, - _: string, - envConfig: Config, - logger: Logger, - tokenIssuer: TokenIssuer, -) { - const providerId = 'okta'; - const secure = envConfig.getBoolean('secure'); - const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const callbackURL = `${baseUrl}/${providerId}/handler/frame`; +export const createOktaProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'okta'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const opts = { - audience, - clientID, - clientSecret, - callbackURL, - }; + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); - if (!opts.clientID || !opts.clientSecret || !opts.audience) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars', - ); - } - - logger.warn( - 'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable', - ); - return undefined; - } - return new OAuthProvider(new OktaAuthProvider(opts), { - disableRefresh: false, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); -} diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index f790228107..aac42d6bd9 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -23,17 +23,15 @@ import { import { executeFrameHandlerStrategy, executeRedirectStrategy, -} from '../../lib/PassportStrategyHelper'; -import { - AuthProviderConfig, - AuthProviderRouteHandlers, PassportDoneCallback, +} from '../../lib/passport'; +import { + AuthProviderRouteHandlers, ProfileInfo, + AuthProviderFactory, } from '../types'; -import { postMessageResponse } from '../../lib/OAuthProvider'; -import { Logger } from 'winston'; +import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity'; -import { Config } from '@backstage/config'; type SamlInfo = { userId: string; @@ -127,15 +125,12 @@ type SAMLProviderOptions = { tokenIssuer: TokenIssuer; }; -export function createSamlProvider( - _authProviderConfig: AuthProviderConfig, - _env: string, - envConfig: Config, - logger: Logger, - tokenIssuer: TokenIssuer, -) { - const entryPoint = envConfig.getString('entryPoint'); - const issuer = envConfig.getString('issuer'); +export const createSamlProvider: AuthProviderFactory = ({ + config, + tokenIssuer, +}) => { + const entryPoint = config.getString('entryPoint'); + const issuer = config.getString('issuer'); const opts = { entryPoint, issuer, @@ -143,11 +138,5 @@ export function createSamlProvider( tokenIssuer, }; - if (!opts.entryPoint || !opts.issuer) { - logger.warn( - 'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable', - ); - return undefined; - } return new SamlAuthProvider(opts); -} +}; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index d372fd1b94..5c05b02739 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -18,74 +18,6 @@ import express from 'express'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity'; import { Config } from '@backstage/config'; -import { OAuthProvider } from '../lib/OAuthProvider'; -import { SamlAuthProvider } from './saml/provider'; - -export type OAuthProviderOptions = { - /** - * Client ID of the auth provider. - */ - clientID: string; - /** - * Client Secret of the auth provider. - */ - clientSecret: string; - /** - * Callback URL to be passed to the auth provider to redirect to after the user signs in. - */ - callbackURL: string; -}; - -export type GenericOAuth2ProviderOptions = OAuthProviderOptions & { - authorizationURL: string; - tokenURL: string; -}; - -export type OAuthProviderConfig = { - /** - * Cookies can be marked with a secure flag to send cookies only when the request - * is over an encrypted channel (HTTPS). - * - * For development environment we don't mark the cookie as secure since we serve - * localhost over HTTP. - */ - secure: boolean; - /** - * The protocol://domain[:port] where the app (frontend) is hosted. This is used to post messages back - * to the window that initiates an auth request. - */ - appOrigin: string; - /** - * Client ID of the auth provider. - */ - clientId: string; - /** - * Client Secret of the auth provider. - */ - clientSecret: string; - /** - * The location of the OAuth Authorization Server - */ - audience?: string; -}; - -export type GenericOAuth2ProviderConfig = OAuthProviderConfig & { - authorizationURL: string; - tokenURL: string; -}; - -export type EnvironmentProviderConfig = { - /** - * key, values are environment names and OAuthProviderConfigs - * - * For e.g - * { - * development: DevelopmentOAuthProviderConfig - * production: ProductionOAuthProviderConfig - * } - */ - [key: string]: OAuthProviderConfig; -}; export type AuthProviderConfig = { /** @@ -93,50 +25,23 @@ export type AuthProviderConfig = { * callbackURL to redirect to once the user signs in to the auth provider. */ baseUrl: string; + + /** + * The base URL of the app as provided by app.baseUrl + */ + appUrl: string; }; -/** - * Any OAuth provider needs to implement this interface which has provider specific - * handlers for different methods to perform authentication, get access tokens, - * refresh tokens and perform sign out. - */ -export interface OAuthProviderHandlers { +export type RedirectInfo = { /** - * This method initiates a sign in request with an auth provider. - * @param {express.Request} req - * @param options + * URL to redirect to */ - start( - req: express.Request, - options: Record, - ): Promise; - + url: string; /** - * Handles the redirect from the auth provider when the user has signed in. - * @param {express.Request} req + * Status code to use for the redirect */ - handler( - req: express.Request, - ): Promise<{ - response: AuthResponse; - refreshToken?: string; - }>; - - /** - * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. - * @param {string} refreshToken - * @param {string} scope - */ - refresh?( - refreshToken: string, - scope: string, - ): Promise>; - - /** - * (Optional) Sign out of the auth provider. - */ - logout?(): Promise; -} + status?: number; +}; /** * Any Auth provider needs to implement this interface which handles the routes in the @@ -203,24 +108,18 @@ export interface AuthProviderRouteHandlers { * @param {express.Response} res */ logout?(req: express.Request, res: express.Response): Promise; - - /** - *(Optional) A method to identify the environment Context of the Request - * - *Request - *- contains the environment context information encoded in the request - * @param {express.Request} req - */ - identifyEnv?(req: express.Request): string | undefined; } +export type AuthProviderFactoryOptions = { + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + tokenIssuer: TokenIssuer; +}; + export type AuthProviderFactory = ( - globalConfig: AuthProviderConfig, - env: string, - envConfig: Config, - logger: Logger, - issuer: TokenIssuer, -) => OAuthProvider | SamlAuthProvider | undefined; + options: AuthProviderFactoryOptions, +) => AuthProviderRouteHandlers; export type AuthResponse = { providerInfo: ProviderInfo; @@ -228,8 +127,6 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentity; }; -export type OAuthResponse = AuthResponse; - export type BackstageIdentity = { /** * The backstage user ID. @@ -242,63 +139,6 @@ export type BackstageIdentity = { idToken?: string; }; -export type OAuthProviderInfo = { - /** - * An access token issued for the signed in user. - */ - accessToken: string; - /** - * (Optional) Id token issued for the signed in user. - */ - idToken?: string; - /** - * Expiry of the access token in seconds. - */ - expiresInSeconds?: number; - /** - * Scopes granted for the access token. - */ - scope: string; -}; - -export type OAuthPrivateInfo = { - /** - * A refresh token issued for the signed in user. - */ - refreshToken: string; -}; - -/** - * Payload sent as a post message after the auth request is complete. - * If successful then has a valid payload with Auth information else contains an error. - */ -export type WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; - -export type PassportDoneCallback = ( - err?: Error, - response?: Res, - privateInfo?: Private, -) => void; - -export type RedirectInfo = { - /** - * URL to redirect to - */ - url: string; - /** - * Status code to use for the redirect - */ - status?: number; -}; - /** * Used to display login information to user, i.e. sidebar popup. * @@ -320,35 +160,3 @@ export type ProfileInfo = { */ picture?: string; }; - -export type RefreshTokenResponse = { - /** - * An access token issued for the signed in user. - */ - accessToken: string; - params: any; -}; - -export type ProviderStrategy = { - userProfile(accessToken: string, callback: Function): void; -}; - -export type SAMLProviderConfig = { - entryPoint: string; - issuer: string; -}; - -export type SAMLEnvironmentProviderConfig = { - [key: string]: SAMLProviderConfig; -}; - -export type OAuthState = { - /* A type for the serialized value in the `state` parameter of the OAuth authorization flow - */ - nonce: string; - env: string; -}; - -export type EnvironmentIdentifierFn = ( - req: express.Request, -) => string | undefined; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 69f058f6db..19a74d47c5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,12 +17,12 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; -import bodyParser from 'body-parser'; import Knex from 'knex'; import { Logger } from 'winston'; import { createAuthProviderRouter } from '../providers'; import { Config } from '@backstage/config'; import { DatabaseKeyStore, TokenFactory, createOidcRouter } from '../identity'; +import { NotFoundError } from '@backstage/backend-common'; export interface RouterOptions { logger: Logger; @@ -36,6 +36,7 @@ export async function createRouter( const router = Router(); const logger = options.logger.child({ plugin: 'auth' }); + const appUrl = options.config.getString('app.baseUrl'); const backendUrl = options.config.getString('backend.baseUrl'); const authUrl = `${backendUrl}/auth`; @@ -52,8 +53,8 @@ export async function createRouter( }); router.use(cookieParser()); - router.use(bodyParser.urlencoded({ extended: false })); - router.use(bodyParser.json()); + router.use(express.urlencoded({ extended: false })); + router.use(express.json()); const providersConfig = options.config.getConfig('auth.providers'); const providers = providersConfig.keys(); @@ -64,14 +65,20 @@ export async function createRouter( const providerConfig = providersConfig.getConfig(providerId); const providerRouter = createAuthProviderRouter( providerId, - { baseUrl: authUrl }, + { baseUrl: authUrl, appUrl }, providerConfig, logger, tokenIssuer, ); router.use(`/${providerId}`, providerRouter); } catch (e) { - logger.error(e.message); + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerId} auth provider, ${e.message}`, + ); + } + + logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); } } @@ -82,5 +89,10 @@ export async function createRouter( }), ); + router.use('/:provider/', req => { + const { provider } = req.params; + throw new NotFoundError(`No auth provider registered for '${provider}'`); + }); + return router; } diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 246cd83d56..7c52dfad72 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -30,5 +30,5 @@ This will launch the full example backend and populate its catalog with some moc ## Links -- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/catalog] -- (The Backstage homepage)[https://backstage.io] +- [Frontend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/catalog) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js b/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js new file mode 100644 index 0000000000..379928493d --- /dev/null +++ b/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + // Adds a single 'bootstrap' location that can be used to trigger work in processors. + // This is primarily here to fulfill foreign key constraints. + await knex('locations').insert({ + id: require('uuid').v4(), + type: 'bootstrap', + target: 'bootstrap', + }); +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + await knex('locations') + .where({ + type: 'bootstrap', + target: 'bootstrap', + }) + .del(); +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 2445ee206e..c1b20ed7bb 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,11 +18,13 @@ "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", "clean": "backstage-cli clean", - "mock-data": "./scripts/mock-data.sh" + "mock-data": "./scripts/mock-data.sh", + "mock-data:local": "./scripts/mock-data-local.sh" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", - "@backstage/catalog-model": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.21", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -39,14 +41,14 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@types/yup": "^0.28.2", "jest-fetch-mock": "^3.0.3", - "msw": "^0.19.5", + "msw": "^0.20.5", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/catalog-backend/scripts/mock-data-local.sh b/plugins/catalog-backend/scripts/mock-data-local.sh new file mode 100755 index 0000000000..3bea819319 --- /dev/null +++ b/plugins/catalog-backend/scripts/mock-data-local.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +for FILE in \ + ../../packages/catalog-model/examples/*.yaml \ +; do \ + curl \ + --location \ + --request POST 'localhost:7000/catalog/locations' \ + --header 'Content-Type: application/json' \ + --data-raw "{\"type\": \"file\", \"target\": \"../catalog-model/${FILE}\"}" + echo +done diff --git a/plugins/catalog-backend/scripts/mock-data.sh b/plugins/catalog-backend/scripts/mock-data.sh index 76f97ccf3a..92ec647281 100755 --- a/plugins/catalog-backend/scripts/mock-data.sh +++ b/plugins/catalog-backend/scripts/mock-data.sh @@ -9,6 +9,8 @@ for URL in \ 'playback-lib-component.yaml' \ 'www-artist-component.yaml' \ 'shuffle-api-component.yaml' \ + 'petstore-api.yaml' \ + 'streetlights-api.yaml' \ ; do \ curl \ --location \ diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 679e5b43c1..958e864a8e 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -17,6 +17,12 @@ import { DatabaseManager } from '../database'; import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +const bootstrapLocation = { + id: expect.any(String), + type: 'bootstrap', + target: 'bootstrap', +}; + describe('DatabaseLocationsCatalog', () => { let catalog: DatabaseLocationsCatalog; @@ -35,9 +41,12 @@ describe('DatabaseLocationsCatalog', () => { await expect( catalog.location('dd12620d-0436-422f-93bd-929aa0788123'), ).resolves.toEqual(expect.objectContaining({ data: location })); - await expect(catalog.locations()).resolves.toEqual([ - expect.objectContaining({ data: location }), - ]); + await expect(catalog.locations()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ data: location }), + expect.objectContaining({ data: bootstrapLocation }), + ]), + ); }); it('does not return duplicates of rows because of logs', async () => { @@ -60,11 +69,12 @@ describe('DatabaseLocationsCatalog', () => { catalog.logUpdateSuccess(location1.id), ).resolves.toBeUndefined(); const locations = await catalog.locations(); - expect(locations.length).toBe(2); + expect(locations.length).toBe(3); expect(locations).toEqual( expect.arrayContaining([ expect.objectContaining({ data: location1 }), expect.objectContaining({ data: location2 }), + expect.objectContaining({ data: bootstrapLocation }), ]), ); }); diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index 21dde4ca74..b86b4ef959 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -24,6 +24,15 @@ import type { DbLocationsRowWithStatus, } from './types'; +const bootstrapLocation = { + id: expect.any(String), + type: 'bootstrap', + target: 'bootstrap', + message: null, + status: null, + timestamp: null, +}; + describe('CommonDatabase', () => { let db: Database; let entityRequest: DbEntityRequest; @@ -85,8 +94,12 @@ describe('CommonDatabase', () => { await db.addLocation(input); const locations = await db.locations(); - expect(locations).toEqual([output]); - const location = await db.location(locations[0].id); + expect(locations).toEqual( + expect.arrayContaining([output, bootstrapLocation]), + ); + const location = await db.location( + locations.find(l => l.type !== 'bootstrap')!.id, + ); expect(location).toEqual(output); // If we add 2 new update log events, @@ -105,20 +118,21 @@ describe('CommonDatabase', () => { DatabaseLocationUpdateLogStatus.FAIL, ); - expect(await db.locations()).toEqual([ - { - ...output, - status: DatabaseLocationUpdateLogStatus.FAIL, - timestamp: expect.any(String), - }, - ]); - - await db.transaction(tx => db.removeLocation(tx, locations[0].id)); - - await expect(db.locations()).resolves.toEqual([]); - await expect(db.location(locations[0].id)).rejects.toThrow( - /Found no location/, + await expect(db.locations()).resolves.toEqual( + expect.arrayContaining([ + bootstrapLocation, + { + ...output, + status: DatabaseLocationUpdateLogStatus.FAIL, + timestamp: expect.any(String), + }, + ]), ); + + await db.transaction(tx => db.removeLocation(tx, location.id)); + + await expect(db.locations()).resolves.toEqual([bootstrapLocation]); + await expect(db.location(location.id)).rejects.toThrow(/Found no location/); }); describe('addEntity', () => { diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 28751f4830..ce91cc737f 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -14,17 +14,16 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common'; import { makeValidator } from '@backstage/catalog-model'; import Knex from 'knex'; -import path from 'path'; import { Logger } from 'winston'; import { CommonDatabase } from './CommonDatabase'; import { Database } from './types'; -const migrationsDir = path.resolve( - require.resolve('@backstage/plugin-catalog-backend/package.json'), - '../migrations', +const migrationsDir = resolvePackagePath( + '@backstage/plugin-catalog-backend', + 'migrations', ); export type CreateDatabaseOptions = { diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts new file mode 100644 index 0000000000..bb5c025c28 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -0,0 +1,217 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocationSpec, Entity } from '@backstage/catalog-model'; +import { CatalogRulesEnforcer } from './CatalogRules'; +import { ConfigReader } from '@backstage/config'; + +const entity = { + user: { + kind: 'User', + } as Entity, + group: { + kind: 'Group', + } as Entity, + component: { + kind: 'component', + } as Entity, + location: { + kind: 'Location', + } as Entity, +}; + +const location: Record = { + x: { + type: 'github', + target: 'https://github.com/a/b/blob/master/x.yaml', + }, + y: { + type: 'github', + target: 'https://github.com/a/b/blob/master/y.yaml', + }, + z: { + type: 'file', + target: '/root/z.yaml', + }, +}; + +describe('CatalogRulesEnforcer', () => { + it('should deny by default', () => { + const enforcer = new CatalogRulesEnforcer([]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(false); + }); + + it('should deny all', () => { + const enforcer = new CatalogRulesEnforcer([{ allow: [] }]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(false); + }); + + it('should allow all', () => { + const enforcer = new CatalogRulesEnforcer([ + { + allow: ['User', 'Group', 'Component', 'Location'].map(kind => ({ + kind, + })), + }, + ]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(true); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(true); + }); + + it('should deny groups', () => { + const enforcer = new CatalogRulesEnforcer([ + { allow: [{ kind: 'User' }, { kind: 'Component' }] }, + ]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + }); + + it('should deny groups from github', () => { + const enforcer = new CatalogRulesEnforcer([ + { allow: [{ kind: 'User' }, { kind: 'Component' }] }, + { allow: [{ kind: 'Group' }], locations: [{ type: 'file' }] }, + ]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + }); + + it('should allow groups from files', () => { + const enforcer = new CatalogRulesEnforcer([ + { allow: [{ kind: 'Group' }], locations: [{ type: 'file' }] }, + ]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + }); + + it('should not be sensitive to kind case', () => { + const enforcer = new CatalogRulesEnforcer([ + { allow: [{ kind: 'group' }] }, + { allow: [{ kind: 'Component' }] }, + ]); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + }); + + describe('fromConfig', () => { + it('should allow components by default', () => { + const enforcer = CatalogRulesEnforcer.fromConfig(new ConfigReader({})); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(true); + }); + + it('should deny all', () => { + const enforcer = CatalogRulesEnforcer.fromConfig( + new ConfigReader({ catalog: { rules: [] } }), + ); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(false); + }); + + it('should allow all', () => { + const enforcer = CatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [{ allow: ['User', 'Group'] }, { allow: ['Component'] }], + }, + }), + ); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(true); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + }); + + it('should deny groups', () => { + const enforcer = CatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { rules: [{ allow: ['User'] }, { allow: ['Component'] }] }, + }), + ); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(false); + }); + + it('should allow groups from a specific github location', () => { + const enforcer = CatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [{ allow: ['user'] }], + locations: [ + { + type: 'github', + target: 'https://github.com/a/b/blob/master/x.yaml', + rules: [ + { + allow: ['Group'], + }, + ], + }, + ], + }, + }), + ); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(false); + }); + + it('should not care about location configuration in catalog.rules', () => { + const enforcer = CatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [{ allow: ['Group'], locations: [{ type: 'github' }] }], + }, + }), + ); + expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); + expect(enforcer.isAllowed(entity.group, location.x)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.y)).toBe(true); + expect(enforcer.isAllowed(entity.group, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + expect(enforcer.isAllowed(entity.location, location.z)).toBe(false); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts new file mode 100644 index 0000000000..e964524bc7 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { LocationSpec, Entity } from '@backstage/catalog-model'; + +/** + * A structure for matching entities to a given rule. + */ +type EntityMatcher = { + kind: string; +}; + +/** + * A structure for matching locations to a given rule. + */ +type LocationMatcher = { + target?: string; + type: string; +}; + +/** + * Rules to apply to catalog entities + * + * An undefined list of matchers means match all, an empty list of matchers means match none + */ +type CatalogRule = { + allow: EntityMatcher[]; + locations?: LocationMatcher[]; +}; + +export class CatalogRulesEnforcer { + /** + * Default rules used by the catalog. + * + * Denies any location from specifying user or group entities. + */ + static readonly defaultRules: CatalogRule[] = [ + { + allow: ['Component', 'API', 'Location'].map(kind => ({ kind })), + }, + ]; + + /** + * Loads catalog rules from config. + * + * This reads `catalog.rules` and defaults to the default rules if no value is present. + * The value of the config should be a list of config objects, each with a single `allow` + * field which in turn is a list of entity kinds to allow. + * + * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog. + * + * It also reads in rules from `catalog.locations`, where each location can have a list + * of rules for that specific location, specified in a `rules` field. + * + * For example: + * + * ```yaml + * catalog: + * rules: + * - allow: [Component, API] + * + * locations: + * - type: github + * target: https://github.com/org/repo/blob/master/users.yaml + * rules: + * - allow: [User, Group] + * - type: github + * target: https://github.com/org/repo/blob/master/systems.yaml + * rules: + * - allow: [System] + * ``` + */ + static fromConfig(config: Config) { + const rules = new Array(); + + if (config.has('catalog.rules')) { + const globalRules = config.getConfigArray('catalog.rules').map(sub => ({ + allow: sub.getStringArray('allow').map(kind => ({ kind })), + })); + rules.push(...globalRules); + } else { + rules.push(...CatalogRulesEnforcer.defaultRules); + } + + if (config.has('catalog.locations')) { + const locationRules = config + .getConfigArray('catalog.locations') + .flatMap(locConf => { + if (!locConf.has('rules')) { + return []; + } + const type = locConf.getString('type'); + const target = locConf.getString('target'); + + return locConf.getConfigArray('rules').map(ruleConf => ({ + allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), + locations: [{ type, target }], + })); + }); + + rules.push(...locationRules); + } + + return new CatalogRulesEnforcer(rules); + } + + constructor(private readonly rules: CatalogRule[]) {} + + /** + * Checks wether a specific entity/location combination is allowed + * according to the configured rules. + */ + isAllowed(entity: Entity, location: LocationSpec) { + for (const rule of this.rules) { + if (!this.matchLocation(location, rule.locations)) { + continue; + } + + if (this.matchEntity(entity, rule.allow)) { + return true; + } + } + + return false; + } + + private matchLocation( + location: LocationSpec, + matchers?: LocationMatcher[], + ): boolean { + if (!matchers) { + return true; + } + + for (const matcher of matchers) { + if (matcher.type !== location.type) { + continue; + } + if (matcher.target && matcher.target !== location.target) { + continue; + } + return true; + } + + return false; + } + + private matchEntity(entity: Entity, matchers?: EntityMatcher[]): boolean { + if (!matchers) { + return true; + } + + for (const matcher of matchers) { + if (entity.kind.toLowerCase() !== matcher.kind.toLowerCase()) { + continue; + } + + return true; + } + + return false; + } +} diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index e5ef410456..bddef6b4de 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Config, ConfigReader } from '@backstage/config'; import { Entity, EntityPolicies, @@ -29,8 +30,11 @@ import { GithubReaderProcessor } from './processors/GithubReaderProcessor'; import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor'; import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor'; import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; +import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor'; +import { AzureApiReaderProcessor } from './processors/AzureApiReaderProcessor'; import { UrlReaderProcessor } from './processors/UrlReaderProcessor'; import { LocationRefProcessor } from './processors/LocationEntityProcessor'; +import { StaticLocationProcessor } from './processors/StaticLocationProcessor'; import * as result from './processors/results'; import { LocationProcessor, @@ -43,26 +47,42 @@ import { } from './processors/types'; import { YamlProcessor } from './processors/YamlProcessor'; import { LocationReader, ReadLocationResult } from './types'; +import { CatalogRulesEnforcer } from './CatalogRules'; // The max amount of nesting depth of generated work items const MAX_DEPTH = 10; +type Options = { + logger?: Logger; + config?: Config; + processors?: LocationProcessor[]; +}; + /** * Implements the reading of a location through a series of processor tasks. */ export class LocationReaders implements LocationReader { private readonly logger: Logger; private readonly processors: LocationProcessor[]; + private readonly rulesEnforcer: CatalogRulesEnforcer; - static defaultProcessors( - entityPolicy: EntityPolicy = new EntityPolicies(), - ): LocationProcessor[] { + static defaultProcessors(options: { + config?: Config; + entityPolicy?: EntityPolicy; + }): LocationProcessor[] { + const { + config = new ConfigReader({}, 'missing-config'), + entityPolicy = new EntityPolicies(), + } = options; return [ + StaticLocationProcessor.fromConfig(config), new FileReaderProcessor(), new GithubReaderProcessor(), - new GithubApiReaderProcessor(), - new GitlabApiReaderProcessor(), + new GithubApiReaderProcessor(config), + new GitlabApiReaderProcessor(config), new GitlabReaderProcessor(), + new BitbucketApiReaderProcessor(config), + new AzureApiReaderProcessor(config), new UrlReaderProcessor(), new YamlProcessor(), new EntityPolicyProcessor(entityPolicy), @@ -71,12 +91,16 @@ export class LocationReaders implements LocationReader { ]; } - constructor( - logger: Logger = getVoidLogger(), - processors: LocationProcessor[] = LocationReaders.defaultProcessors(), - ) { + constructor({ + logger = getVoidLogger(), + config, + processors = LocationReaders.defaultProcessors({ config }), + }: Options) { this.logger = logger; this.processors = processors; + this.rulesEnforcer = config + ? CatalogRulesEnforcer.fromConfig(config) + : new CatalogRulesEnforcer(CatalogRulesEnforcer.defaultRules); } async read(location: LocationSpec): Promise { @@ -93,11 +117,20 @@ export class LocationReaders implements LocationReader { } else if (item.type === 'data') { await this.handleData(item, emit); } else if (item.type === 'entity') { - const entity = await this.handleEntity(item, emit); - output.entities.push({ - entity, - location: item.location, - }); + if (this.rulesEnforcer.isAllowed(item.entity, item.location)) { + const entity = await this.handleEntity(item, emit); + output.entities.push({ + entity, + location: item.location, + }); + } else { + output.errors.push({ + location: item.location, + error: new Error( + `Entity of kind ${item.entity.kind} is not allowed from location ${item.location.target}:${item.location.type}`, + ), + }); + } } else if (item.type === 'error') { await this.handleError(item, emit); output.errors.push({ diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts new file mode 100644 index 0000000000..9b234e3e75 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AzureApiReaderProcessor } from './AzureApiReaderProcessor'; +import { ConfigReader } from '@backstage/config'; + +describe('AzureApiReaderProcessor', () => { + const createConfig = (token: string | undefined) => + ConfigReader.fromConfigs([ + { + context: '', + data: { + catalog: { + processors: { + azureApi: { + privateToken: token, + }, + }, + }, + }, + }, + ]); + + it('should build raw api', () => { + const processor = new AzureApiReaderProcessor(createConfig(undefined)); + const tests = [ + { + target: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', + url: new URL( + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + ), + err: undefined, + }, + { + target: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', + url: new URL( + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + ), + err: undefined, + }, + { + target: 'https://api.com/a/b/blob/master/path/to/c.yaml', + url: null, + err: + 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + }, + { + target: 'com/a/b/blob/master/path/to/c.yaml', + url: null, + err: + 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + }, + ]; + + for (const test of tests) { + if (test.err) { + expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err); + } else if (test.url) { + expect(processor.buildRawUrl(test.target).toString()).toEqual( + test.url.toString(), + ); + } else { + throw new Error( + 'This should not have happened. Either err or url should have matched.', + ); + } + } + }); + + it('should return request options', () => { + const tests = [ + { + token: '0123456789', + expect: { + headers: { + Authorization: 'Basic OjAxMjM0NTY3ODk=', + }, + }, + }, + { + token: '', + expect: { + headers: {}, + }, + err: + "Invalid type in config for key 'catalog.processors.azureApi.privateToken' in '', got empty-string, wanted string", + }, + { + token: undefined, + expect: { + headers: {}, + }, + }, + ]; + + for (const test of tests) { + if (test.err) { + expect( + () => new AzureApiReaderProcessor(createConfig(test.token)), + ).toThrowError(test.err); + } else { + const processor = new AzureApiReaderProcessor(createConfig(test.token)); + expect(processor.getRequestOptions()).toEqual(test.expect); + } + } + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts new file mode 100644 index 0000000000..03ff30ea69 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocationSpec } from '@backstage/catalog-model'; +import fetch, { RequestInit, HeadersInit } from 'node-fetch'; +import * as result from './results'; +import { LocationProcessor, LocationProcessorEmit } from './types'; +import { Config } from '@backstage/config'; + +export class AzureApiReaderProcessor implements LocationProcessor { + private privateToken: string; + + constructor(config: Config) { + this.privateToken = + config.getOptionalString('catalog.processors.azureApi.privateToken') ?? + ''; + } + + getRequestOptions(): RequestInit { + const headers: HeadersInit = {}; + + if (this.privateToken !== '') { + headers.Authorization = `Basic ${Buffer.from( + `:${this.privateToken}`, + 'utf8', + ).toString('base64')}`; + } + + const requestOptions: RequestInit = { + headers, + }; + + return requestOptions; + } + + async readLocation( + location: LocationSpec, + optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'azure/api') { + return false; + } + + try { + const url = this.buildRawUrl(location.target); + + const response = await fetch(url.toString(), this.getRequestOptions()); + + // for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html + if (response.ok && response.status !== 203) { + const data = await response.buffer(); + emit(result.data(location, data)); + } else { + const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + if (!optional) { + emit(result.notFoundError(location, message)); + } + } else { + emit(result.generalError(location, message)); + } + } + } catch (e) { + const message = `Unable to read ${location.type} ${location.target}, ${e}`; + emit(result.generalError(location, message)); + } + return true; + } + + // Converts + // from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents + // to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} + buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + project, + srcKeyword, + repoName, + ] = url.pathname.split('/'); + + const path = url.searchParams.get('path') || ''; + const ref = url.searchParams.get('version')?.substr(2); + + if ( + url.hostname !== 'dev.azure.com' || + empty !== '' || + userOrOrg === '' || + project === '' || + srcKeyword !== '_git' || + repoName === '' || + path === '' || + ref === '' || + !path.match(/\.yaml$/) + ) { + throw new Error('Wrong Azure Devops URL or Invalid file path'); + } + + // transform to api + url.pathname = [ + empty, + userOrOrg, + project, + '_apis', + 'git', + 'repositories', + repoName, + 'items', + ].join('/'); + + const queryParams = [`path=${path}`]; + + if (ref) { + queryParams.push(`version=${ref}`); + } + + url.search = queryParams.join('&'); + + url.protocol = 'https'; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.test.ts new file mode 100644 index 0000000000..953f0703be --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.test.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BitbucketApiReaderProcessor } from './BitbucketApiReaderProcessor'; +import { ConfigReader } from '@backstage/config'; + +describe('BitbucketApiReaderProcessor', () => { + const createConfig = ( + username: string | undefined, + appPassword: string | undefined, + ) => + ConfigReader.fromConfigs([ + { + context: '', + data: { + catalog: { + processors: { + bitbucketApi: { + username: username, + appPassword: appPassword, + }, + }, + }, + }, + }, + ]); + + it('should build raw api', () => { + const processor = new BitbucketApiReaderProcessor( + createConfig(undefined, undefined), + ); + + const tests = [ + { + target: + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + url: new URL( + 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', + ), + err: undefined, + }, + { + target: 'https://api.com/a/b/blob/master/path/to/c.yaml', + url: null, + err: + 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Bitbucket URL or Invalid file path', + }, + { + target: 'com/a/b/blob/master/path/to/c.yaml', + url: null, + err: + 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + }, + ]; + + for (const test of tests) { + if (test.err) { + expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err); + } else if (test.url) { + expect(processor.buildRawUrl(test.target).toString()).toEqual( + test.url.toString(), + ); + } else { + throw new Error( + 'This should not have happened. Either err or url should have matched.', + ); + } + } + }); + + it('should return request options', () => { + const tests = [ + { + username: '', + password: '', + expect: { + headers: {}, + }, + err: + "Invalid type in config for key 'catalog.processors.bitbucketApi.username' in '', got empty-string, wanted string", + }, + { + username: 'only-user-provided', + password: '', + expect: { + headers: {}, + }, + err: + "Invalid type in config for key 'catalog.processors.bitbucketApi.appPassword' in '', got empty-string, wanted string", + }, + { + username: '', + password: 'only-password-provided', + expect: { + headers: {}, + }, + err: + "Invalid type in config for key 'catalog.processors.bitbucketApi.username' in '', got empty-string, wanted string", + }, + { + username: 'some-user', + password: 'my-secret', + expect: { + headers: { + Authorization: 'Basic c29tZS11c2VyOm15LXNlY3JldA==', + }, + }, + }, + { + username: undefined, + password: undefined, + expect: { + headers: {}, + }, + }, + { + username: 'only-user-provided', + password: undefined, + expect: { + headers: {}, + }, + }, + { + username: undefined, + password: 'only-password-provided', + expect: { + headers: {}, + }, + }, + ]; + + for (const test of tests) { + if (test.err) { + expect( + () => + new BitbucketApiReaderProcessor( + createConfig(test.username, test.password), + ), + ).toThrowError(test.err); + } else { + const processor = new BitbucketApiReaderProcessor( + createConfig(test.username, test.password), + ); + expect(processor.getRequestOptions()).toEqual(test.expect); + } + } + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts new file mode 100644 index 0000000000..97ecf0cd1f --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocationSpec } from '@backstage/catalog-model'; +import fetch, { RequestInit, HeadersInit } from 'node-fetch'; +import * as result from './results'; +import { LocationProcessor, LocationProcessorEmit } from './types'; +import { Config } from '@backstage/config'; + +export class BitbucketApiReaderProcessor implements LocationProcessor { + private username: string; + private password: string; + + constructor(config: Config) { + this.username = + config.getOptionalString('catalog.processors.bitbucketApi.username') ?? + ''; + this.password = + config.getOptionalString('catalog.processors.bitbucketApi.appPassword') ?? + ''; + } + + getRequestOptions(): RequestInit { + const headers: HeadersInit = {}; + + if (this.username !== '' && this.password !== '') { + headers.Authorization = `Basic ${Buffer.from( + `${this.username}:${this.password}`, + 'utf8', + ).toString('base64')}`; + } + + const requestOptions: RequestInit = { + headers, + }; + + return requestOptions; + } + + async readLocation( + location: LocationSpec, + optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'bitbucket/api') { + return false; + } + + try { + const url = this.buildRawUrl(location.target); + + const response = await fetch(url.toString(), this.getRequestOptions()); + + if (response.ok) { + const data = await response.buffer(); + emit(result.data(location, data)); + } else { + const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + if (!optional) { + emit(result.notFoundError(location, message)); + } + } else { + emit(result.generalError(location, message)); + } + } + } catch (e) { + const message = `Unable to read ${location.type} ${location.target}, ${e}`; + emit(result.generalError(location, message)); + } + return true; + } + + // Converts + // from: https://bitbucket.org/orgname/reponame/src/master/file.yaml + // to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml + + buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + repoName, + srcKeyword, + ref, + ...restOfPath + ] = url.pathname.split('/'); + + if ( + url.hostname !== 'bitbucket.org' || + empty !== '' || + userOrOrg === '' || + repoName === '' || + srcKeyword !== 'src' || + !restOfPath.join('/').match(/\.yaml$/) + ) { + throw new Error('Wrong Bitbucket URL or Invalid file path'); + } + + // transform to api + url.pathname = [ + empty, + '2.0', + 'repositories', + userOrOrg, + repoName, + 'src', + ref, + ...restOfPath, + ].join('/'); + url.hostname = 'api.bitbucket.org'; + url.protocol = 'https'; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts index 4dd3fd359f..51c3cf4cd0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.test.ts @@ -15,10 +15,27 @@ */ import { GithubApiReaderProcessor } from './GithubApiReaderProcessor'; +import { ConfigReader } from '@backstage/config'; describe('GithubApiReaderProcessor', () => { + const createConfig = (token: string | undefined) => + ConfigReader.fromConfigs([ + { + context: '', + data: { + catalog: { + processors: { + githubApi: { + privateToken: token, + }, + }, + }, + }, + }, + ]); + it('should build raw api', () => { - const processor = new GithubApiReaderProcessor(); + const processor = new GithubApiReaderProcessor(createConfig(undefined)); const tests = [ { @@ -53,8 +70,14 @@ describe('GithubApiReaderProcessor', () => { for (const test of tests) { if (test.err) { expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err); + } else if (test.url) { + expect(processor.buildRawUrl(test.target).toString()).toEqual( + test.url.toString(), + ); } else { - expect(processor.buildRawUrl(test.target)).toEqual(test.url); + throw new Error( + 'This should not have happened. Either err or url should have matched.', + ); } } }); @@ -72,6 +95,16 @@ describe('GithubApiReaderProcessor', () => { }, { token: '', + err: + "Invalid type in config for key 'catalog.processors.githubApi.privateToken' in '', got empty-string, wanted string", + expect: { + headers: { + Accept: 'application/vnd.github.v3.raw', + }, + }, + }, + { + token: undefined, expect: { headers: { Accept: 'application/vnd.github.v3.raw', @@ -81,9 +114,16 @@ describe('GithubApiReaderProcessor', () => { ]; for (const test of tests) { - process.env.GITHUB_PRIVATE_TOKEN = test.token; - const processor = new GithubApiReaderProcessor(); - expect(processor.getRequestOptions()).toEqual(test.expect); + if (test.err) { + expect( + () => new GithubApiReaderProcessor(createConfig(test.token)), + ).toThrowError(test.err); + } else { + const processor = new GithubApiReaderProcessor( + createConfig(test.token), + ); + expect(processor.getRequestOptions()).toEqual(test.expect); + } } }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts index ff61d004ca..f8d1d6caf8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubApiReaderProcessor.ts @@ -18,9 +18,16 @@ import { LocationSpec } from '@backstage/catalog-model'; import fetch, { RequestInit, HeadersInit } from 'node-fetch'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; +import { Config } from '@backstage/config'; export class GithubApiReaderProcessor implements LocationProcessor { - private privateToken: string = process.env.GITHUB_PRIVATE_TOKEN || ''; + private privateToken: string; + + constructor(config: Config) { + this.privateToken = + config.getOptionalString('catalog.processors.githubApi.privateToken') ?? + ''; + } getRequestOptions(): RequestInit { const headers: HeadersInit = { diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts index b83c9a16f3..9f38d782fe 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts @@ -15,11 +15,35 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch from 'node-fetch'; +import fetch, { RequestInit, HeadersInit } from 'node-fetch'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; +import { Config } from '@backstage/config'; export class GithubReaderProcessor implements LocationProcessor { + private privateToken: string; + + constructor(config?: Config) { + this.privateToken = + config?.getOptionalString('catalog.processors.github.privateToken') ?? ''; + } + + getRequestOptions(): RequestInit { + const headers: HeadersInit = { + Accept: 'application/vnd.github.v3.raw', + }; + + if (this.privateToken !== '') { + headers.Authorization = `token ${this.privateToken}`; + } + + const requestOptions: RequestInit = { + headers, + }; + + return requestOptions; + } + async readLocation( location: LocationSpec, optional: boolean, @@ -34,7 +58,7 @@ export class GithubReaderProcessor implements LocationProcessor { // TODO(freben): Should "hard" errors thrown by this line be treated as // notFound instead of fatal? - const response = await fetch(url.toString()); + const response = await fetch(url.toString(), this.getRequestOptions()); if (response.ok) { const data = await response.buffer(); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts index a429a0e6fb..48ed270049 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts @@ -15,17 +15,34 @@ */ import { GitlabApiReaderProcessor } from './GitlabApiReaderProcessor'; +import { ConfigReader } from '@backstage/config'; describe('GitlabApiReaderProcessor', () => { + const createConfig = (token: string | undefined) => + ConfigReader.fromConfigs([ + { + context: '', + data: { + catalog: { + processors: { + gitlabApi: { + privateToken: token, + }, + }, + }, + }, + }, + ]); + it('should build raw api', () => { - const processor = new GitlabApiReaderProcessor(); + const processor = new GitlabApiReaderProcessor(createConfig(undefined)); const tests = [ { target: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', url: new URL( - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch', + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', ), err: undefined, }, @@ -33,7 +50,7 @@ describe('GitlabApiReaderProcessor', () => { target: 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', url: new URL( - 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch', + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', ), err: undefined, }, @@ -41,7 +58,7 @@ describe('GitlabApiReaderProcessor', () => { target: 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup url: new URL( - 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch', + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', ), err: undefined, }, @@ -50,7 +67,7 @@ describe('GitlabApiReaderProcessor', () => { 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/', url: null, err: - 'Incorrect url: https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/, Error: Gitlab url does not end in .ya?ml', + 'Incorrect url: https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/, Error: GitLab url does not end in .ya?ml', }, ]; @@ -59,8 +76,14 @@ describe('GitlabApiReaderProcessor', () => { expect(() => processor.buildRawUrl(test.target, 12345)).toThrowError( test.err, ); + } else if (test.url) { + expect(processor.buildRawUrl(test.target, 12345).toString()).toEqual( + test.url.toString(), + ); } else { - expect(processor.buildRawUrl(test.target, 12345)).toEqual(test.url); + throw new Error( + 'This should not have happened. Either err or url should have matched.', + ); } } }); @@ -77,6 +100,16 @@ describe('GitlabApiReaderProcessor', () => { }, { token: '', + err: + "Invalid type in config for key 'catalog.processors.gitlabApi.privateToken' in '', got empty-string, wanted string", + expect: { + headers: { + 'PRIVATE-TOKEN': '', + }, + }, + }, + { + token: undefined, expect: { headers: { 'PRIVATE-TOKEN': '', @@ -86,9 +119,16 @@ describe('GitlabApiReaderProcessor', () => { ]; for (const test of tests) { - process.env.GITLAB_PRIVATE_TOKEN = test.token; - const processor = new GitlabApiReaderProcessor(); - expect(processor.getRequestOptions()).toEqual(test.expect); + if (test.err) { + expect( + () => new GitlabApiReaderProcessor(createConfig(test.token)), + ).toThrowError(test.err); + } else { + const processor = new GitlabApiReaderProcessor( + createConfig(test.token), + ); + expect(processor.getRequestOptions()).toEqual(test.expect); + } } }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts index 6964640eb4..067edba3d9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts @@ -18,9 +18,16 @@ import { LocationSpec } from '@backstage/catalog-model'; import fetch, { RequestInit, HeadersInit } from 'node-fetch'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; +import { Config } from '@backstage/config'; export class GitlabApiReaderProcessor implements LocationProcessor { - private privateToken: string = process.env.GITLAB_PRIVATE_TOKEN || ''; + private privateToken: string; + + constructor(config: Config) { + this.privateToken = + config.getOptionalString('catalog.processors.gitlabApi.privateToken') ?? + ''; + } getRequestOptions(): RequestInit { const headers: HeadersInit = { 'PRIVATE-TOKEN': '' }; @@ -77,7 +84,7 @@ export class GitlabApiReaderProcessor implements LocationProcessor { const branchAndfilePath = url.pathname.split('/-/blob/')[1]; if (!branchAndfilePath.match(/\.ya?ml$/)) { - throw new Error('Gitlab url does not end in .ya?ml'); + throw new Error('GitLab url does not end in .ya?ml'); } const [branch, ...filePath] = branchAndfilePath.split('/'); @@ -127,7 +134,7 @@ export class GitlabApiReaderProcessor implements LocationProcessor { return projectID; } catch (e) { - throw new Error(`Could not get Gitlab ProjectID for: ${target}, ${e}`); + throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`); } } } diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts index 211f325afe..9f308ee184 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts @@ -77,7 +77,7 @@ export class GitlabReaderProcessor implements LocationProcessor { blobKeyword !== 'blob' || !restOfPath.join('/').match(/\.yaml$/) ) { - throw new Error('Wrong Gitlab URL'); + throw new Error('Wrong GitLab URL'); } // Replace 'blob' with 'raw' diff --git a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts new file mode 100644 index 0000000000..6a2d1096cc --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocationSpec } from '@backstage/catalog-model'; +import * as result from './results'; +import { Config } from '@backstage/config'; +import { LocationProcessorEmit } from './types'; + +export class StaticLocationProcessor implements StaticLocationProcessor { + static fromConfig(config: Config): StaticLocationProcessor { + const locations: LocationSpec[] = []; + + const lConfigs = config.getOptionalConfigArray('catalog.locations') ?? []; + for (const lConfig of lConfigs) { + const type = lConfig.getString('type'); + const target = lConfig.getString('target'); + locations.push({ type, target }); + } + + return new StaticLocationProcessor(locations); + } + + constructor(private readonly staticLocations: LocationSpec[]) {} + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'bootstrap') { + return false; + } + + for (const staticLocation of this.staticLocations) { + emit(result.location(staticLocation, false)); + } + + return true; + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts new file mode 100644 index 0000000000..9f1ede3f9e --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { YamlProcessor } from './YamlProcessor'; +import { Entity } from '@backstage/catalog-model'; +import yaml from 'yaml'; +import { TextEncoder } from 'util'; +import { + LocationProcessorEntityResult, + LocationProcessorErrorResult, +} from './types'; + +describe('YamlProcessor', () => { + const processor = new YamlProcessor(); + const locationSpec = { + type: 'url', + target: 'http://example.com/component.yaml', + }; + + function encodeEntity(entity: string): Buffer { + const data = new TextEncoder().encode(entity); + return Buffer.from(data); + } + + it('should only process files with yaml', async () => { + const wrongLocationSpec = { + type: 'url', + target: 'http://example.com/component.json', + }; + + const buffer = Buffer.from([]); + const never = jest.fn(); + + expect(await processor.parseData(buffer, wrongLocationSpec, never)).toBe( + false, + ); + + expect(never).not.toBeCalled(); + }); + + it('should process url that contains yaml', async () => { + const containsYamlLocationSpec = { + type: 'url', + target: 'http://example.com/component?path=test.yaml&c=1&d=2', + }; + + const buffer = Buffer.from([]); + const emit = jest.fn(); + + expect( + await processor.parseData(buffer, containsYamlLocationSpec, emit), + ).toBe(true); + + expect(emit).toBeCalled(); + }); + + it('should process entity with yaml', async () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + spec: {}, + } as Entity; + + const buffer = encodeEntity(yaml.stringify(entity)); + const emit = jest.fn(); + + expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); + + const e = emit.mock.calls[0][0] as LocationProcessorEntityResult; + expect(e.type).toBe('entity'); + expect(e.location).toBe(locationSpec); + expect(e.entity).toEqual(entity); + }); + + it('should process multiple entities with yaml', async () => { + const entityComponent = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + spec: {}, + } as Entity; + + const entityApi = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'my-api', + }, + spec: {}, + } as Entity; + + const buffer = encodeEntity( + `${yaml.stringify(entityComponent)}---\n${yaml.stringify(entityApi)}`, + ); + const emit = jest.fn(); + + expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); + + const eComponent = emit.mock.calls[0][0] as LocationProcessorEntityResult; + expect(eComponent.type).toBe('entity'); + expect(eComponent.location).toBe(locationSpec); + expect(eComponent.entity).toEqual(entityComponent); + + const eApi = emit.mock.calls[1][0] as LocationProcessorEntityResult; + expect(eApi.type).toBe('entity'); + expect(eApi.location).toBe(locationSpec); + expect(eApi.entity).toEqual(entityApi); + }); + + it('should fail process entity on invalid yaml', async () => { + const buffer = encodeEntity('{'); + const emit = jest.fn(); + + expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); + + const e = emit.mock.calls[0][0] as LocationProcessorErrorResult; + expect(e.error.message).toMatch(/^YAML error, /); + expect(e.type).toBe('error'); + expect(e.location).toBe(locationSpec); + }); + + it('should fail process entity if not object at root', async () => { + const buffer = encodeEntity('[]'); + const emit = jest.fn(); + + expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); + + const e = emit.mock.calls[0][0] as LocationProcessorErrorResult; + expect(e.error.message).toMatch(/^Expected object at root, got /); + expect(e.type).toBe('error'); + expect(e.location).toBe(locationSpec); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts index 6a2b5cf419..79ae55fae1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts @@ -26,7 +26,7 @@ export class YamlProcessor implements LocationProcessor { location: LocationSpec, emit: LocationProcessorEmit, ): Promise { - if (!location.target.match(/\.ya?ml$/)) { + if (!location.target.match(/\.ya?ml/)) { return false; } diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 5c7ccb23f5..0b532d1664 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; +import { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { Server } from 'http'; import { Logger } from 'winston'; import { HigherOrderOperations } from '..'; @@ -34,12 +38,13 @@ export async function startStandaloneServer( options: ServerOptions, ): Promise { const logger = options.logger.child({ service: 'catalog-backend' }); + const config = ConfigReader.fromConfigs(await loadBackendConfig()); logger.debug('Creating application...'); const db = await DatabaseManager.createInMemoryDatabase({ logger }); const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationReader = new LocationReaders(); + const locationReader = new LocationReaders({ logger, config }); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index 52bbd0d0c3..cff632cb24 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -9,5 +9,5 @@ supply the base views to show and manage them. ## Links -- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/catalog-backend] -- (The Backstage homepage)[https://backstage.io] +- [Backend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/catalog-backend) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index a0e69a647d..b088e1699b 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,27 +21,31 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.18", - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/plugin-github-actions": "^0.1.1-alpha.18", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.18", - "@backstage/plugin-sentry": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/plugin-api-docs": "^0.1.1-alpha.21", + "@backstage/plugin-github-actions": "^0.1.1-alpha.21", + "@backstage/plugin-jenkins": "^0.1.1-alpha.21", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.21", + "@backstage/plugin-techdocs": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "swr": "^0.2.2" + "swr": "^0.3.0", + "@types/react": "^16.9" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", @@ -49,9 +53,9 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.19.0", + "msw": "^0.20.5", "react-test-renderer": "^16.13.1", - "whatwg-fetch": "^2.0.0" + "whatwg-fetch": "^3.4.0" }, "files": [ "dist" diff --git a/plugins/catalog/src/api/CatalogClient.test.ts b/plugins/catalog/src/api/CatalogClient.test.ts index 7803f2e173..18f062db77 100644 --- a/plugins/catalog/src/api/CatalogClient.test.ts +++ b/plugins/catalog/src/api/CatalogClient.test.ts @@ -18,25 +18,21 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; import { Entity } from '@backstage/catalog-model'; +import { UrlPatternDiscovery } from '@backstage/core'; const server = setupServer(); +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); describe('CatalogClient', () => { beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); - const mockApiOrigin = 'http://backstage:9191'; - const mockBasePath = '/i-am-a-mock-base'; - let client = new CatalogClient({ - apiOrigin: mockApiOrigin, - basePath: mockBasePath, - }); + + let client = new CatalogClient({ discoveryApi }); beforeEach(() => { - client = new CatalogClient({ - apiOrigin: mockApiOrigin, - basePath: mockBasePath, - }); + client = new CatalogClient({ discoveryApi }); }); describe('getEntiies', () => { @@ -61,7 +57,7 @@ describe('CatalogClient', () => { beforeEach(() => { server.use( - rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => { + rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => { return res(ctx.json(defaultResponse)); }), ); @@ -75,15 +71,12 @@ describe('CatalogClient', () => { it('builds entity search filters properly', async () => { expect.assertions(2); server.use( - rest.get( - `${mockApiOrigin}${mockBasePath}/entities`, - (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'a=1&b=2&b=3&%C3%B6=%3D', - ); - return res(ctx.json([])); - }, - ), + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'a=1&b=2&b=3&%C3%B6=%3D', + ); + return res(ctx.json([])); + }), ); const entities = await client.getEntities({ diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 3804315ace..d5ff033caa 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -20,24 +20,17 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { CatalogApi, EntityCompoundName } from './types'; +import { DiscoveryApi } from '@backstage/core'; export class CatalogClient implements CatalogApi { - private apiOrigin: string; - private basePath: string; + private readonly discoveryApi: DiscoveryApi; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; } private async getRequired(path: string): Promise { - const url = `${this.apiOrigin}${this.basePath}${path}`; + const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url); if (!response.ok) { @@ -50,7 +43,7 @@ export class CatalogClient implements CatalogApi { } private async getOptional(path: string): Promise { - const url = `${this.apiOrigin}${this.basePath}${path}`; + const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url); if (!response.ok) { @@ -100,7 +93,7 @@ export class CatalogClient implements CatalogApi { async addLocation(type: string, target: string) { const response = await fetch( - `${this.apiOrigin}${this.basePath}/locations`, + `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { headers: { 'Content-Type': 'application/json', @@ -135,7 +128,7 @@ export class CatalogClient implements CatalogApi { async removeEntityByUid(uid: string): Promise { const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities/by-uid/${uid}`, + `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { method: 'DELETE', }, diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx new file mode 100644 index 0000000000..bf69818995 --- /dev/null +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { AboutCard } from './AboutCard'; + +describe('', () => { + it('renders info and "view source" link', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'backstage.io/managed-by-location': + 'github:https://github.com/spotify/backstage/blob/master/software.yaml', + }, + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const { getByText } = render(); + expect(getByText('service')).toBeInTheDocument(); + expect(getByText('View Source').closest('a')).toHaveAttribute( + 'href', + 'https://github.com/spotify/backstage/blob/master/software.yaml', + ); + }); +}); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx new file mode 100644 index 0000000000..d83beaedb5 --- /dev/null +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -0,0 +1,183 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + Grid, + Typography, + makeStyles, + Chip, + IconButton, + Card, + CardContent, + CardHeader, + Divider, +} from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; + +import GitHubIcon from '@material-ui/icons/GitHub'; +import { IconLinkVertical } from './IconLinkVertical'; +import EditIcon from '@material-ui/icons/Edit'; +import DocsIcon from '@material-ui/icons/Description'; + +const useStyles = makeStyles(theme => ({ + links: { + margin: theme.spacing(2, 0), + display: 'grid', + gridAutoFlow: 'column', + gridAutoColumns: 'min-content', + gridGap: theme.spacing(3), + }, + label: { + color: theme.palette.text.secondary, + textTransform: 'uppercase', + fontSize: '10px', + fontWeight: 'bold', + letterSpacing: 0.5, + overflow: 'hidden', + whiteSpace: 'nowrap', + }, + value: { + fontWeight: 'bold', + overflow: 'hidden', + lineHeight: '24px', + wordBreak: 'break-word', + }, + description: { + wordBreak: 'break-word', + }, +})); + +const iconMap: Record = { + github: , +}; + +type CodeLinkInfo = { icon?: React.ReactNode; href?: string }; + +function getCodeLinkInfo(entity: Entity): CodeLinkInfo { + const location = + entity?.metadata?.annotations?.['backstage.io/managed-by-location']; + + if (location) { + // split by first `:` + // e.g. "github:https://github.com/spotify/backstage/blob/master/software.yaml" + const [type, target] = location.split(/:(.+)/); + + return { icon: iconMap[type], href: target }; + } + return {}; +} + +type AboutCardProps = { + entity: Entity; +}; + +export function AboutCard({ entity }: AboutCardProps) { + const classes = useStyles(); + const codeLink = getCodeLinkInfo(entity); + + return ( + + + + + } + subheader={ + + } + /> + + + + + + {entity?.metadata?.description || 'No description'} + + + + + + + {(entity?.metadata?.tags || []).map(t => ( + + ))} + + + + + ); +} + +function AboutField({ + label, + value, + gridSizes, + children, +}: { + label: string; + value?: string; + gridSizes?: Record; + children?: React.ReactNode; +}) { + const classes = useStyles(); + + // Content is either children or a string prop `value` + const content = React.Children.count(children) ? ( + children + ) : ( + + {value || `unknown`} + + ); + return ( + + + {label} + + {content} + + ); +} diff --git a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx new file mode 100644 index 0000000000..91a5211925 --- /dev/null +++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { makeStyles, Link } from '@material-ui/core'; +import LinkIcon from '@material-ui/icons/Link'; + +export type IconLinkVerticalProps = { + icon?: React.ReactNode; + href?: string; + label: string; +}; + +const useIconStyles = makeStyles({ + link: { + display: 'grid', + justifyItems: 'center', + gridGap: 4, + textAlign: 'center', + }, + label: { + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + }, +}); + +export function IconLinkVertical({ + icon = , + href = '#', + ...props +}: IconLinkVerticalProps) { + const classes = useIconStyles(); + return ( + + {icon} + {props.label} + + ); +} diff --git a/plugins/catalog/src/components/AboutCard/IconLinkVertical/index.ts b/plugins/catalog/src/components/AboutCard/IconLinkVertical/index.ts new file mode 100644 index 0000000000..ddc146dbd8 --- /dev/null +++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { IconLinkVertical } from './IconLinkVertical'; diff --git a/plugins/catalog/src/components/AboutCard/index.ts b/plugins/catalog/src/components/AboutCard/index.ts new file mode 100644 index 0000000000..93765ff942 --- /dev/null +++ b/plugins/catalog/src/components/AboutCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { AboutCard } from './AboutCard'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index e58b2d407b..db490cf542 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -15,23 +15,26 @@ */ import { + configApiRef, Content, ContentHeader, + errorApiRef, identityApiRef, SupportButton, - configApiRef, useApi, } from '@backstage/core'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { catalogApiRef } from '../../api/types'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntites'; -import { CatalogFilter, ButtonGroup } from '../CatalogFilter/CatalogFilter'; +import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; +import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import CatalogLayout from './CatalogLayout'; import { CatalogTabs, LabeledComponentType } from './CatalogTabs'; import { WelcomeBanner } from './WelcomeBanner'; @@ -47,13 +50,37 @@ const useStyles = makeStyles(theme => ({ const CatalogPageContents = () => { const styles = useStyles(); - const { loading, error, matchingEntities } = useFilteredEntities(); + const { + loading, + error, + reload, + matchingEntities, + availableTags, + } = useFilteredEntities(); + const configApi = useApi(configApiRef); + const catalogApi = useApi(catalogApiRef); + const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); const [selectedSidebarItem, setSelectedSidebarItem] = useState(); - const orgName = - useApi(configApiRef).getOptionalString('organization.name') ?? 'Company'; + const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; + + const addMockData = useCallback(async () => { + try { + const promises: Promise[] = []; + const root = configApi.getConfig('catalog.exampleEntityLocations'); + for (const type of root.keys()) { + for (const target of root.getStringArray(type)) { + promises.push(catalogApi.addLocation(type, target)); + } + } + await Promise.all(promises); + await reload(); + } catch (err) { + errorApi.post(err); + } + }, [catalogApi, configApi, errorApi, reload]); const tabs = useMemo( () => [ @@ -140,12 +167,14 @@ const CatalogPageContents = () => { onChange={({ label }) => setSelectedSidebarItem(label)} initiallySelected="owned" /> +
diff --git a/plugins/catalog/src/components/CatalogPage/index.ts b/plugins/catalog/src/components/CatalogPage/index.ts new file mode 100644 index 0000000000..1adb8866c8 --- /dev/null +++ b/plugins/catalog/src/components/CatalogPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { CatalogPage } from './CatalogPage'; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 707213f3bb..d1eef2a005 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -15,12 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; -const entites: Entity[] = [ +const entities: Entity[] = [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -40,13 +39,14 @@ const entites: Entity[] = [ describe('CatalogTable component', () => { it('should render error message when error is passed in props', async () => { - const rendered = render( + const rendered = await renderWithEffects( wrapInTestApp( {}} />, ), ); @@ -57,12 +57,13 @@ describe('CatalogTable component', () => { }); it('should display entity names when loading has finished and no error occurred', async () => { - const rendered = render( + const rendered = await renderWithEffects( wrapInTestApp( {}} />, ), ); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index b5afbcaa0b..cd4df13775 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -15,11 +15,12 @@ */ import { Entity, LocationSpec } from '@backstage/catalog-model'; import { Table, TableColumn, TableProps } from '@backstage/core'; -import { Link } from '@material-ui/core'; +import { Chip, Link } from '@material-ui/core'; +import Add from '@material-ui/icons/Add'; import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import { Alert } from '@material-ui/lab'; -import React from 'react'; +import React, { Dispatch } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntites'; @@ -45,6 +46,7 @@ const columns: TableColumn[] = [ .filter(Boolean) .join(':'), kind: entity.kind, + selectedTabId: 'overview', })} > {entity.metadata.name} @@ -63,6 +65,21 @@ const columns: TableColumn[] = [ title: 'Description', field: 'metadata.description', }, + { + title: 'Tags', + field: 'metadata.tags', + cellStyle: { + padding: '0px 16px 0px 20px', + }, + render: (entity: Entity) => ( + <> + {entity.metadata.tags && + entity.metadata.tags.map(t => ( + + ))} + + ), + }, ]; type CatalogTableProps = { @@ -70,6 +87,7 @@ type CatalogTableProps = { titlePreamble: string; loading: boolean; error?: any; + onAddMockData: Dispatch; }; export const CatalogTable = ({ @@ -77,6 +95,7 @@ export const CatalogTable = ({ loading, error, titlePreamble, + onAddMockData, }: CatalogTableProps) => { const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); @@ -132,6 +151,13 @@ export const CatalogTable = ({ onClick: () => toggleStarredEntity(rowData), }; }, + { + icon: () => , + tooltip: 'Add example components', + isFreeAction: true, + onClick: onAddMockData, + hidden: !(entities && entities.length === 0), + }, ]; return ( diff --git a/plugins/catalog/src/components/EntityPageApi/EntityPageApi.tsx b/plugins/catalog/src/components/EntityPageApi/EntityPageApi.tsx new file mode 100644 index 0000000000..5c0ff5485b --- /dev/null +++ b/plugins/catalog/src/components/EntityPageApi/EntityPageApi.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model'; +import { Content, Progress, useApi } from '@backstage/core'; +import { ApiDefinitionCard } from '@backstage/plugin-api-docs'; +import { Grid } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React, { FC } from 'react'; +import { useAsync } from 'react-use'; +import { catalogApiRef } from '../..'; + +export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => { + const catalogApi = useApi(catalogApiRef); + + const { value: apiEntities, loading } = useAsync(async () => { + const a = await Promise.all( + ((entity?.spec?.implementsApis as string[]) || []).map(api => + catalogApi.getEntityByName({ + kind: 'API', + name: api, + }), + ), + ); + const b = new Map(); + + a.filter(api => !!api).forEach(api => { + b.set(api?.metadata?.name!, api as ApiEntityV1alpha1); + }); + return b; + }, [catalogApi, entity]); + + return ( + + {loading && } + {!loading && ( + + {((entity?.spec?.implementsApis as string[]) || []).map(api => { + const apiEntity = apiEntities && apiEntities.get(api); + + return ( + + {!apiEntity && ( + + Error on fetching the API: {api} + + )} + + {apiEntity && ( + + )} + + ); + })} + + )} + + ); +}; diff --git a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx new file mode 100644 index 0000000000..3261764d11 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO(shmidt-i): move to the app +import { Entity } from '@backstage/catalog-model'; +import { Content } from '@backstage/core'; +import { LatestWorkflowsForBranchCard } from '@backstage/plugin-github-actions'; +import { Grid } from '@material-ui/core'; +import React, { FC } from 'react'; + +export const EntityPageCi: FC<{ entity: Entity }> = ({ entity }) => { + return ( + + + {entity.metadata?.annotations?.['backstage.io/github-actions-id'] && ( + + + + )} + + + ); +}; diff --git a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx b/plugins/catalog/src/components/EntityPageDocs/EntityDocsPage.tsx similarity index 56% rename from plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx rename to plugins/catalog/src/components/EntityPageDocs/EntityDocsPage.tsx index 4bb64f221d..ceba7e3200 100644 --- a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx +++ b/plugins/catalog/src/components/EntityPageDocs/EntityDocsPage.tsx @@ -14,19 +14,21 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { render } from '@testing-library/react'; import React from 'react'; -import { EntityMetadataCard } from './EntityMetadataCard'; +import { Entity } from '@backstage/catalog-model'; +import { Reader } from '@backstage/plugin-techdocs'; +import { Content } from '@backstage/core'; -describe('EntityMetadataCard component', () => { - it('should display entity name if provided', async () => { - const testEntity: Entity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { name: 'test' }, - }; - const rendered = await render(); - expect(await rendered.findByText('test')).toBeInTheDocument(); - }); -}); +export const EntityPageDocs = ({ entity }: { entity: Entity }) => { + return ( + + + + ); +}; diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx similarity index 53% rename from plugins/catalog/src/components/EntityPage/EntityPage.tsx rename to plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index c11844f722..6a1b80150e 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -13,34 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import React, { useState, useContext } from 'react'; +import { useParams, useNavigate } from 'react-router'; -import { Entity } from '@backstage/catalog-model'; +import { EntityContext } from '../../hooks/useEntity'; import { - Content, - errorApiRef, - Header, - HeaderLabel, - HeaderTabs, - Page, pageTheme, PageTheme, + Page, + Header, + HeaderLabel, + Content, Progress, - useApi, } from '@backstage/core'; -import { SentryIssuesWidget } from '@backstage/plugin-sentry'; -import { Widget as GithubActionsWidget } from '@backstage/plugin-github-actions'; -import { Grid, Box } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import React, { FC, useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useAsync } from 'react-use'; -import { catalogApiRef } from '../..'; -import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard'; -import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; +import { Entity } from '@backstage/catalog-model'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; +import { Box } from '@material-ui/core'; +import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; +import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; +import { Alert } from '@material-ui/lab'; +import { Tabbed } from './Tabbed'; + +const getPageTheme = (entity?: Entity): PageTheme => { + const themeKey = entity?.spec?.type?.toString() ?? 'home'; + return pageTheme[themeKey] ?? pageTheme.home; +}; + +const EntityPageTitle = ({ + entity, + title, +}: { + title: string; + entity: Entity | undefined; +}) => ( + + {title} + {entity && } + +); -const REDIRECT_DELAY = 1000; function headerProps( kind: string, namespace: string | undefined, @@ -60,52 +71,27 @@ function headerProps( }; } -export const getPageTheme = (entity?: Entity): PageTheme => { - const themeKey = entity?.spec?.type?.toString() ?? 'home'; - return pageTheme[themeKey] ?? pageTheme.home; -}; - -const EntityPageTitle: FC<{ title: string; entity: Entity | undefined }> = ({ - entity, - title, -}) => ( - - {title} - {entity && } - -); - -export const EntityPage: FC<{}> = () => { +export const EntityPageLayout = ({ + children, +}: { + children?: React.ReactNode; +}) => { const { optionalNamespaceAndName, kind } = useParams() as { optionalNamespaceAndName: string; kind: string; }; - const navigate = useNavigate(); const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); - const { value: entity, error, loading } = useAsync( - () => catalogApi.getEntityByName({ kind, namespace, name }), - [catalogApi, kind, namespace, name], + const { entity, loading, error } = useContext(EntityContext); + const { headerTitle, headerType } = headerProps( + kind, + namespace, + name, + entity!, ); - useEffect(() => { - if (!error && !loading && !entity) { - errorApi.post(new Error('Entity not found!')); - setTimeout(() => { - navigate('/'); - }, REDIRECT_DELAY); - } - }, [errorApi, navigate, error, loading, entity]); - - if (!name) { - navigate('/catalog'); - return null; - } - + const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); + const navigate = useNavigate(); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); navigate('/'); @@ -113,45 +99,10 @@ export const EntityPage: FC<{}> = () => { const showRemovalDialog = () => setConfirmationDialogOpen(true); - // TODO - Replace with proper tabs implementation - const tabs = [ - { - id: 'overview', - label: 'Overview', - }, - { - id: 'ci', - label: 'CI/CD', - }, - { - id: 'tests', - label: 'Tests', - }, - { - id: 'api', - label: 'API', - }, - { - id: 'monitoring', - label: 'Monitoring', - }, - { - id: 'quality', - label: 'Quality', - }, - ]; - - const { headerTitle, headerType } = headerProps( - kind, - namespace, - name, - entity, - ); - return ( - +
} + title={} pageTitleOverride={headerTitle} type={headerType} > @@ -172,45 +123,20 @@ export const EntityPage: FC<{}> = () => { {loading && } + {entity && {children}} + {error && ( {error.toString()} )} - - {entity && ( - <> - - - - - - - - - - - {entity.metadata?.annotations?.[ - 'backstage.io/github-actions-id' - ] && ( - - - - )} - - - - setConfirmationDialogOpen(false)} - /> - - )} + setConfirmationDialogOpen(false)} + /> ); }; +EntityPageLayout.Content = Tabbed.Content; diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx new file mode 100644 index 0000000000..d5ca9170a2 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Tabbed } from './Tabbed'; +import { renderInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import { Routes, Route } from 'react-router'; + +describe('Tabbed layout', () => { + it('renders simplest case', async () => { + const rendered = await renderInTestApp( + + tabbed-test-content} + /> + , + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + }); + + it('throws if any other component is a child of Tabbed.Layout', async () => { + await expect( + renderInTestApp( + + tabbed-test-content} + /> +
This will cause app to throw
+
, + ), + ).rejects.toThrow(/This component only accepts/); + }); + + it('navigates when user clicks different tab', async () => { + const rendered = await renderInTestApp( + + + tabbed-test-content} + /> + tabbed-test-content-2} + /> + + } + /> + , + ); + + const secondTab = rendered.queryAllByRole('tab')[1]; + act(() => { + fireEvent.click(secondTab); + }); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + }); + + describe('correctly delegates nested links', () => { + const renderRoute = (route: string) => + renderInTestApp( + + + tabbed-test-content} + /> + + tabbed-test-content-2 + + tabbed-test-nested-content-2} + /> + + + } + /> + + } + /> + , + { routeEntries: [route] }, + ); + + it('works for nested content', async () => { + const rendered = await renderRoute('/some-other-path/nested'); + + expect( + rendered.queryByText('tabbed-test-content'), + ).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + expect( + rendered.queryByText('tabbed-test-nested-content-2'), + ).toBeInTheDocument(); + }); + + it('works for non-nested content', async () => { + const rendered = await renderRoute('/some-other-path/'); + + expect( + rendered.queryByText('tabbed-test-content'), + ).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + expect( + rendered.queryByText('tabbed-test-nested-content-2'), + ).not.toBeInTheDocument(); + }); + }); + + it('shows only one tab contents at a time', async () => { + const rendered = await renderInTestApp( + + tabbed-test-content} + /> + tabbed-test-content-2} + /> + , + { routeEntries: ['/some-other-path'] }, + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + }); + + it('redirects to the top level when no route is matching the url', async () => { + const rendered = await renderInTestApp( + + tabbed-test-content} + /> + tabbed-test-content-2} + /> + , + { routeEntries: ['/non-existing-path'] }, + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + + expect( + rendered.queryByText('tabbed-test-content-2'), + ).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx new file mode 100644 index 0000000000..096b2fa708 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx @@ -0,0 +1,127 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + useParams, + useNavigate, + PartialRouteObject, + matchRoutes, + RouteObject, + useRoutes, + Navigate, + RouteMatch, +} from 'react-router'; +import { Tab, HeaderTabs, Content } from '@backstage/core'; +import { Helmet } from 'react-helmet'; + +const getSelectedIndexOrDefault = ( + matchedRoute: RouteMatch, + tabs: Tab[], + defaultIndex = 0, +) => { + if (!matchedRoute) return defaultIndex; + const tabIndex = tabs.findIndex(t => t.id === matchedRoute.route.path); + return ~tabIndex ? tabIndex : defaultIndex; +}; + +/** + * Compound component, which allows you to define layout + * for EntityPage using Tabs as a subnavigation mechanism + * Constists of 2 parts: Tabbed.Layout and Tabbed.Content. + * Takes care of: tabs, routes, document titles, spacing around content + * + * @example + * ```jsx + * + * This is rendered under /example/anything-here route} + * /> + * + * ``` + */ +export const Tabbed = { + Layout: ({ children }: { children: React.ReactNode }) => { + const routes: PartialRouteObject[] = []; + const tabs: Tab[] = []; + const params = useParams(); + const navigate = useNavigate(); + + React.Children.forEach(children, child => { + if (!React.isValidElement(child)) { + // Skip conditionals resolved to falses/nulls/undefineds etc + return; + } + if (child.type !== Tabbed.Content) { + throw new Error( + 'This component only accepts Content elements as direct children. Check the code of the EntityPage.', + ); + } + const pathAndId = (child as JSX.Element).props.path; + + // Child here must be then always a functional component without any wrappers + tabs.push({ + id: pathAndId, + label: (child as JSX.Element).props.title, + }); + + routes.push({ + path: pathAndId, + element: child.props.element, + }); + }); + + // Add catch-all for incorrect sub-routes + if ((routes?.[0]?.path ?? '') !== '') + routes.push({ + path: '/*', + element: , + }); + + const [matchedRoute] = + matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? []; + const selectedIndex = getSelectedIndexOrDefault(matchedRoute, tabs); + const currentTab = tabs[selectedIndex]; + const title = currentTab?.label; + + const onTabChange = (index: number) => + // Remove trailing /* + // And remove leading / for relative navigation + // Note! route resolves relative to the position in the React tree, + // not relative to current location + navigate(tabs[index].id.replace(/\/\*$/, '').replace(/^\//, '')); + + const currentRouteElement = useRoutes(routes); + + if (!currentTab) return null; + return ( + <> + + + + {currentRouteElement} + + + ); + }, + Content: (_props: { path: string; title: string; element: JSX.Element }) => + null, +}; diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/index.ts b/plugins/catalog/src/components/EntityPageLayout/Tabbed/index.ts new file mode 100644 index 0000000000..d58cd626ba --- /dev/null +++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { Tabbed } from './Tabbed'; diff --git a/plugins/catalog/src/components/EntityPageLayout/index.ts b/plugins/catalog/src/components/EntityPageLayout/index.ts new file mode 100644 index 0000000000..acf6f948f1 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageLayout/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityPageLayout } from './EntityPageLayout'; diff --git a/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx b/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx new file mode 100644 index 0000000000..9b29b205cf --- /dev/null +++ b/plugins/catalog/src/components/EntityPageOverview/EntityPageOverview.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO(shmidt-i): move to the app +import { Entity } from '@backstage/catalog-model'; +import { Content } from '@backstage/core'; +import { LatestWorkflowRunCard } from '@backstage/plugin-github-actions'; +import { + JenkinsBuildsWidget, + JenkinsLastBuildWidget, +} from '@backstage/plugin-jenkins'; +import { Grid } from '@material-ui/core'; +import React, { FC } from 'react'; +import { AboutCard } from '../AboutCard'; + +export const EntityPageOverview: FC<{ entity: Entity }> = ({ entity }) => { + return ( + + + + + + {entity.metadata?.annotations?.[ + 'backstage.io/jenkins-github-folder' + ] && ( + + + + )} + {entity.metadata?.annotations?.[ + 'backstage.io/jenkins-github-folder' + ] && ( + + + + )} + {entity.metadata?.annotations?.['backstage.io/github-actions-id'] && ( + + + + )} + + + ); +}; diff --git a/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx new file mode 100644 index 0000000000..e328b64d44 --- /dev/null +++ b/plugins/catalog/src/components/EntityProvider/EntityProvider.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ReactNode } from 'react'; +import { useEntityFromUrl, EntityContext } from '../../hooks/useEntity'; + +export const EntityProvider = ({ children }: { children: ReactNode }) => { + const { entity, loading, error } = useEntityFromUrl(); + + return ( + + {children} + + ); +}; diff --git a/plugins/catalog/src/components/EntityProvider/index.ts b/plugins/catalog/src/components/EntityProvider/index.ts new file mode 100644 index 0000000000..01eae4737f --- /dev/null +++ b/plugins/catalog/src/components/EntityProvider/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityProvider } from './EntityProvider'; diff --git a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx new file mode 100644 index 0000000000..6f294c54ff --- /dev/null +++ b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + ApiProvider, + ApiRegistry, + IdentityApi, + identityApiRef, + storageApiRef, +} from '@backstage/core'; +import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { CatalogApi, catalogApiRef } from '../../api/types'; +import { EntityFilterGroupsProvider } from '../../filter'; +import { ResultsFilter } from './ResultsFilter'; + +describe('Results Filter', () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity1', + tags: ['java'], + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity2', + }, + spec: { + owner: 'not-tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity3', + tags: ['java', 'test'], + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + ] as Entity[]), + }; + + const indentityApi: Partial = { + getUserId: () => 'tools@example.com', + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children}, + , + ), + ); + + it('should render all available tags', async () => { + const tags = ['test', 'java']; + const { findByText } = renderWrapped( + , + ); + for (const tag of tags) { + expect(await findByText(tag)).toBeInTheDocument(); + } + }); +}); diff --git a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx new file mode 100644 index 0000000000..8c5737b7b6 --- /dev/null +++ b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Button, + Checkbox, + Divider, + List, + ListItem, + ListItemText, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; +import React, { useCallback, useContext, useState } from 'react'; +import { filterGroupsContext } from '../../filter/context'; + +const useStyles = makeStyles(theme => ({ + filterBox: { + display: 'flex', + margin: theme.spacing(2, 0, 0, 0), + }, + filterBoxTitle: { + margin: theme.spacing(1, 0, 0, 1), + fontWeight: 'bold', + flex: 1, + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + checkbox: { + padding: theme.spacing(0, 1, 0, 1), + }, +})); + +type Props = { + availableTags: string[]; +}; + +/** + * The additional results filter in the sidebar. + */ +export const ResultsFilter = ({ availableTags }: Props) => { + const classes = useStyles(); + + const [selectedTags, setSelectedTags] = useState([]); + const context = useContext(filterGroupsContext); + if (!context) { + throw new Error(`Must be used inside an EntityFilterGroupsProvider`); + } + const setSelectedTagsFilter = context?.setSelectedTags; + + const updateSelectedTags = useCallback( + (tags: string[]) => { + setSelectedTags(tags); + setSelectedTagsFilter(tags); + }, + [setSelectedTags, setSelectedTagsFilter], + ); + + return ( + <> +
+ + Refine Results + {' '} + +
+ + + Tags + + + {availableTags.map(t => { + const labelId = `checkbox-list-label-${t}`; + return ( + + updateSelectedTags( + selectedTags.includes(t) + ? selectedTags.filter(s => s !== t) + : [...selectedTags, t], + ) + } + > + + + + ); + })} + + + ); +}; diff --git a/plugins/catalog/src/components/Router.tsx b/plugins/catalog/src/components/Router.tsx new file mode 100644 index 0000000000..61855f56d9 --- /dev/null +++ b/plugins/catalog/src/components/Router.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ComponentType } from 'react'; +import { CatalogPage } from './CatalogPage'; +import { EntityPageLayout } from './EntityPageLayout'; +import { Route, Routes } from 'react-router'; +import { entityRoute, rootRoute } from '../routes'; +import { Content } from '@backstage/core'; +import { Typography, Link } from '@material-ui/core'; +import { EntityProvider } from './EntityProvider'; +import { useEntity } from '../hooks/useEntity'; + +const DefaultEntityPage = () => ( + + + This is default entity page. + + To override this component with your custom implementation, read + docs on{' '} + + backstage.io/docs + + + + } + /> + +); + +const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => { + const { entity } = useEntity(); + // Loading and error states + if (!entity) return ; + + // Otherwise EntityPage provided from the App + // Note that EntityPage will include EntityPageLayout already + return ; +}; + +export const Router = ({ + EntityPage = DefaultEntityPage, +}: { + EntityPage?: ComponentType; +}) => ( + + } /> + + + + } + /> + +); diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx index dd557a4322..94bc428a45 100644 --- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx @@ -16,8 +16,8 @@ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; -import React, { useCallback, useRef, useState } from 'react'; -import { useAsync } from 'react-use'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAsyncFn } from 'react-use'; import { catalogApiRef } from '../api/types'; import { filterGroupsContext, FilterGroupsContext } from './context'; import { @@ -46,7 +46,9 @@ export const EntityFilterGroupsProvider = ({ // The hook that implements the actual context building function useProvideEntityFilters(): FilterGroupsContext { const catalogApi = useApi(catalogApiRef); - const { value: entities, error } = useAsync(() => catalogApi.getEntities()); + const [{ value: entities, error }, doReload] = useAsyncFn(() => + catalogApi.getEntities(), + ); const filterGroups = useRef<{ [filterGroupId: string]: FilterGroup; @@ -54,16 +56,23 @@ function useProvideEntityFilters(): FilterGroupsContext { const selectedFilterKeys = useRef<{ [filterGroupId: string]: Set; }>({}); + const selectedTags = useRef([]); const [filterGroupStates, setFilterGroupStates] = useState<{ [filterGroupId: string]: FilterGroupStates; }>({}); const [matchingEntities, setMatchingEntities] = useState([]); + const [availableTags, setAvailableTags] = useState([]); + + useEffect(() => { + doReload(); + }, [doReload]); const rebuild = useCallback(() => { setFilterGroupStates( buildStates( filterGroups.current, selectedFilterKeys.current, + selectedTags.current, entities, error, ), @@ -72,9 +81,11 @@ function useProvideEntityFilters(): FilterGroupsContext { buildMatchingEntities( filterGroups.current, selectedFilterKeys.current, + selectedTags.current, entities, ), ); + setAvailableTags(collectTags(entities)); }, [entities, error]); const register = useCallback( @@ -109,14 +120,29 @@ function useProvideEntityFilters(): FilterGroupsContext { [rebuild], ); + const setSelectedTags = useCallback( + (tags: string[]) => { + selectedTags.current = tags; + rebuild(); + }, + [rebuild], + ); + + const reload = useCallback(async () => { + await doReload(); + }, [doReload]); + return { register, unregister, setGroupSelectedFilters, + setSelectedTags, + reload, loading: !error && !entities, error, filterGroupStates, matchingEntities, + availableTags, }; } @@ -125,6 +151,7 @@ function useProvideEntityFilters(): FilterGroupsContext { function buildStates( filterGroups: { [filterGroupId: string]: FilterGroup }, selectedFilterKeys: { [filterGroupId: string]: Set }, + selectedTags: string[], entities?: Entity[], error?: Error, ): { [filterGroupId: string]: FilterGroupStates } { @@ -153,6 +180,7 @@ function buildStates( const otherMatchingEntities = buildMatchingEntities( filterGroups, selectedFilterKeys, + selectedTags, entities, filterGroupId, ); @@ -170,11 +198,23 @@ function buildStates( return result; } +// Given all entites, find all possible tags and provide them in a sorted list. +function collectTags(entities?: Entity[]): string[] { + const tags = new Set(); + (entities || []).forEach(e => { + if (e.metadata.tags) { + e.metadata.tags.forEach(t => tags.add(t)); + } + }); + return Array.from(tags).sort(); +} + // Given all filter groups and what filters are actually selected, extract all // entities that match all those filter groups. function buildMatchingEntities( filterGroups: { [filterGroupId: string]: FilterGroup }, selectedFilterKeys: { [filterGroupId: string]: Set }, + selectedTags: string[], entities?: Entity[], excludeFilterGroupId?: string, ): Entity[] { @@ -201,6 +241,16 @@ function buildMatchingEntities( } } + // Filter by tags, if at least one tag is selected. Include all entities + // that have at least one of the selected tags + if (selectedTags.length > 0) { + allFilters.push( + entity => + !!entity.metadata.tags && + entity.metadata.tags.some(t => selectedTags.includes(t)), + ); + } + // All filter groups that had any checked filters need to match. Note that // every() always returns true for an empty array. return entities?.filter(entity => allFilters.every(fn => fn(entity))) ?? []; diff --git a/plugins/catalog/src/filter/context.ts b/plugins/catalog/src/filter/context.ts index 31e9c91b97..838ec8d532 100644 --- a/plugins/catalog/src/filter/context.ts +++ b/plugins/catalog/src/filter/context.ts @@ -26,10 +26,13 @@ export type FilterGroupsContext = { ) => void; unregister: (filterGroupId: string) => void; setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; + setSelectedTags: (tags: string[]) => void; + reload: () => Promise; loading: boolean; error?: Error; filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; matchingEntities: Entity[]; + availableTags: string[]; }; /** diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx index 60e7d4f412..acf4790ebf 100644 --- a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx +++ b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx @@ -51,12 +51,12 @@ describe('useEntityFilterGroup', () => { it('works for an empty set of filters', async () => { catalogApi.getEntities.mockResolvedValue([]); const group: FilterGroup = { filters: {} }; - const { result, wait } = renderHook( + const { result, waitFor } = renderHook( () => useEntityFilterGroup('g1', group), { wrapper }, ); - await wait(() => expect(result.current.state.type).toBe('ready')); + await waitFor(() => expect(result.current.state.type).toBe('ready')); }); it('works for a single group', async () => { @@ -73,12 +73,12 @@ describe('useEntityFilterGroup', () => { f2: e => e.metadata.name !== 'n', }, }; - const { result, wait } = renderHook( + const { result, waitFor } = renderHook( () => useEntityFilterGroup('g1', group), { wrapper }, ); - await wait(() => expect(result.current.state.type).toEqual('ready')); + await waitFor(() => expect(result.current.state.type).toEqual('ready')); let state = result.current.state as FilterGroupStatesReady; expect(state.state.filters.f1).toEqual({ isSelected: false, @@ -91,7 +91,7 @@ describe('useEntityFilterGroup', () => { act(() => result.current.setSelectedFilters(['f1'])); - await wait(() => expect(result.current.state.type).toEqual('ready')); + await waitFor(() => expect(result.current.state.type).toEqual('ready')); state = result.current.state as FilterGroupStatesReady; expect(state.state.filters.f1).toEqual({ isSelected: true, @@ -104,7 +104,7 @@ describe('useEntityFilterGroup', () => { act(() => result.current.setSelectedFilters(['f2'])); - await wait(() => expect(result.current.state.type).toEqual('ready')); + await waitFor(() => expect(result.current.state.type).toEqual('ready')); state = result.current.state as FilterGroupStatesReady; expect(state.state.filters.f1).toEqual({ isSelected: false, diff --git a/plugins/catalog/src/filter/useFilteredEntities.ts b/plugins/catalog/src/filter/useFilteredEntities.ts index 256f5247c7..138a7547c0 100644 --- a/plugins/catalog/src/filter/useFilteredEntities.ts +++ b/plugins/catalog/src/filter/useFilteredEntities.ts @@ -30,5 +30,7 @@ export function useFilteredEntities() { loading: context.loading, error: context.error, matchingEntities: context.matchingEntities, + availableTags: context.availableTags, + reload: context.reload, }; } diff --git a/plugins/catalog/src/hooks/useEntity.ts b/plugins/catalog/src/hooks/useEntity.ts new file mode 100644 index 0000000000..53b1cff73a --- /dev/null +++ b/plugins/catalog/src/hooks/useEntity.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useEffect, createContext, useContext } from 'react'; +import { useNavigate, useParams } from 'react-router'; +import { useApi, errorApiRef } from '@backstage/core'; +import { catalogApiRef } from '../api/types'; +import { useAsync } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; + +const REDIRECT_DELAY = 2000; + +type EntityLoadingStatus = { + entity?: Entity; + loading: boolean; + error?: Error; +}; + +export const EntityContext = createContext({ + entity: undefined, + loading: true, + error: undefined, +}); + +export const useEntityFromUrl = (): EntityLoadingStatus => { + const { optionalNamespaceAndName, kind } = useParams(); + const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); + const navigate = useNavigate(); + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { value: entity, error, loading } = useAsync( + () => catalogApi.getEntityByName({ kind, namespace, name }), + [catalogApi, kind, namespace, name], + ); + + useEffect(() => { + if (error || (!loading && !entity)) { + errorApi.post(new Error('Entity not found!')); + setTimeout(() => { + navigate('/'); + }, REDIRECT_DELAY); + } + + if (!name) { + errorApi.post(new Error('No name provided!')); + navigate('/'); + } + }, [errorApi, navigate, error, loading, entity, name]); + + return { entity, loading, error }; +}; + +/** + * Always going to return an entity, or throw an error if not a descendant of a EntityProvider. + */ +export const useEntity = () => { + const { entity } = useContext<{ entity: Entity }>(EntityContext as any); + return { entity }; +}; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 36fd69971d..a6d86e8102 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -19,3 +19,7 @@ export * from './api/CatalogClient'; export * from './api/types'; export * from './routes'; export { useEntityCompoundName } from './components/useEntityCompoundName'; +export { Router } from './components/Router'; +export { useEntity } from './hooks/useEntity'; +export { AboutCard } from './components/AboutCard'; +export { EntityPageLayout } from './components/EntityPageLayout'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 8f4ac80f48..4bfbf2453b 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -15,14 +15,7 @@ */ import { createPlugin } from '@backstage/core'; -import { CatalogPage } from './components/CatalogPage/CatalogPage'; -import { EntityPage } from './components/EntityPage/EntityPage'; -import { entityRoute, rootRoute } from './routes'; export const plugin = createPlugin({ id: 'catalog', - register({ router }) { - router.addRoute(rootRoute, CatalogPage); - router.addRoute(entityRoute, EntityPage); - }, }); diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 69c4e651b4..14e23ff73b 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -20,11 +20,11 @@ const NoIcon = () => null; export const rootRoute = createRouteRef({ icon: NoIcon, - path: '/', + path: '', title: 'Catalog', }); export const entityRoute = createRouteRef({ icon: NoIcon, - path: '/catalog/:kind/:optionalNamespaceAndName/', + path: ':kind/:optionalNamespaceAndName/*', title: 'Entity', }); diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 4fb7a28124..b5824d3869 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,8 +36,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/CircleCIWidget.tsx similarity index 73% rename from plugins/circleci/src/components/App.tsx rename to plugins/circleci/src/components/CircleCIWidget.tsx index a3f7112132..767eb6d5c7 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/components/CircleCIWidget.tsx @@ -15,25 +15,11 @@ */ import React from 'react'; import { Route, MemoryRouter, Routes } from 'react-router'; -import { BuildsPage, Builds } from '../pages/BuildsPage'; -import { DetailedViewPage, BuildWithSteps } from '../pages/BuildWithStepsPage'; +import { Builds } from '../pages/BuildsPage'; +import { BuildWithSteps } from '../pages/BuildWithStepsPage'; import { AppStateProvider } from '../state'; import { Settings } from './Settings'; -export const App = () => { - return ( - - <> - - } /> - } /> - - - - - ); -}; - // TODO: allow pass in settings as props // When some shared settings workflow // will be established diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index 11f2c80b88..e2e6c4fa69 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -17,4 +17,4 @@ export { plugin } from './plugin'; export * from './api'; export * from './route-refs'; -export { CircleCIWidget } from './components/App'; +export { CircleCIWidget } from './components/CircleCIWidget'; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 77e6daf755..d0e0c655c3 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -25,6 +25,8 @@ import { Layout } from '../../components/Layout'; import LaunchIcon from '@material-ui/icons/Launch'; import { useSettings } from '../../state/useSettings'; import { useBuildWithSteps } from '../../state/useBuildWithSteps'; +import { AppStateProvider } from '../../state'; +import { Settings } from '../../components/Settings'; const IconLink = IconButton as typeof Link; const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( @@ -93,11 +95,14 @@ const pickClassName = ( }; const Page = () => ( - - - - - + + + + + + + + ); const BuildWithStepsView: FC<{}> = () => { diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx index 7b124c03c1..6e7fc44249 100644 --- a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -19,13 +19,18 @@ import { Grid } from '@material-ui/core'; import { Builds as BuildsComp } from './lib/Builds'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; +import { AppStateProvider } from '../../state/AppState'; +import { Settings } from '../../components/Settings'; const BuildsPage: FC<{}> = () => ( - - - - - + + + + + + + + ); const Builds = () => ( diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 6a2e37df10..47ecccef4f 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,12 +14,14 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import { App } from './components/App'; -import { circleCIRouteRef } from './route-refs'; +import { circleCIRouteRef, circleCIBuildRouteRef } from './route-refs'; +import BuildsPage from './pages/BuildsPage/BuildsPage'; +import BuildWithStepsPage from './pages/BuildWithStepsPage/BuildWithStepsPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.addRoute(circleCIRouteRef, App, { exact: false }); + router.addRoute(circleCIRouteRef, BuildsPage); + router.addRoute(circleCIBuildRouteRef, BuildWithStepsPage); }, }); diff --git a/plugins/circleci/src/route-refs.tsx b/plugins/circleci/src/route-refs.tsx index d87ad2fbad..581f035c1d 100644 --- a/plugins/circleci/src/route-refs.tsx +++ b/plugins/circleci/src/route-refs.tsx @@ -34,5 +34,10 @@ const CircleCIIcon: FC = props => ( export const circleCIRouteRef = createRouteRef({ icon: CircleCIIcon, path: '/circleci', - title: 'CircleCI', + title: 'CircleCI | All builds', +}); + +export const circleCIBuildRouteRef = createRouteRef({ + path: '/circleci/build/:buildId', + title: 'CircleCI | Build info', }); diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index 6d38c5f901..282ef68f12 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -70,7 +70,6 @@ export const transform = ( export function useBuilds() { const [{ repo, owner, token }] = useSettings(); - const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); diff --git a/plugins/explore/package.json b/plugins/explore/package.json index e7783675ae..42176c45cf 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,9 +32,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/explore/src/components/ExplorePluginPage.tsx b/plugins/explore/src/components/ExplorePluginPage.tsx index 1752f402be..3e073ad322 100644 --- a/plugins/explore/src/components/ExplorePluginPage.tsx +++ b/plugins/explore/src/components/ExplorePluginPage.tsx @@ -37,6 +37,14 @@ const useStyles = makeStyles(theme => ({ })); const toolsCards = [ + { + title: 'New Relic', + description: + 'Observability platform built to help engineers create and monitor their software', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'performance', 'monitoring', 'errors', 'alerting'], + }, { title: 'CircleCI', description: diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 8fcc4591d2..e4f09976eb 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ "mock-data": "./scripts/mock-data.sh" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.18", - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/core-api": "^0.1.1-alpha.18", - "@backstage/plugin-catalog": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/core-api": "^0.1.1-alpha.21", + "@backstage/plugin-catalog": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,12 +35,13 @@ "moment": "^2.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-actions/scripts/sample.yaml b/plugins/github-actions/scripts/sample.yaml index d582bf9c35..7d9de50463 100644 --- a/plugins/github-actions/scripts/sample.yaml +++ b/plugins/github-actions/scripts/sample.yaml @@ -5,6 +5,7 @@ metadata: description: backstage.io annotations: github.com/project-slug: 'spotify/backstage' + backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git spec: type: website lifecycle: production diff --git a/plugins/github-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts index acdfb90cf3..9a95f21860 100644 --- a/plugins/github-actions/src/api/GithubActionsApi.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -23,7 +23,7 @@ import { export const githubActionsApiRef = createApiRef({ id: 'plugin.githubactions.service', - description: 'Used by the Github Actions plugin to make requests', + description: 'Used by the GitHub Actions plugin to make requests', }); export type GithubActionsApi = { diff --git a/plugins/github-actions/src/components/Widget/Widget.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx similarity index 87% rename from plugins/github-actions/src/components/Widget/Widget.tsx rename to plugins/github-actions/src/components/Cards/Cards.tsx index 031364c8ce..478b3810cb 100644 --- a/plugins/github-actions/src/components/Widget/Widget.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -15,7 +15,7 @@ */ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; -import { WorkflowRun } from '../WorkflowRunsTable'; +import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { @@ -74,7 +74,7 @@ const WidgetContent = ({ ); }; -export const Widget = ({ +export const LatestWorkflowRunCard = ({ entity, branch = 'master', }: { @@ -108,3 +108,15 @@ export const Widget = ({ ); }; + +export const LatestWorkflowsForBranchCard = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => ( + + + +); diff --git a/plugins/github-actions/src/components/Cards/index.ts b/plugins/github-actions/src/components/Cards/index.ts new file mode 100644 index 0000000000..8c987ea1d5 --- /dev/null +++ b/plugins/github-actions/src/components/Cards/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { LatestWorkflowRunCard, LatestWorkflowsForBranchCard } from './Cards'; diff --git a/plugins/github-actions/src/components/Router.tsx b/plugins/github-actions/src/components/Router.tsx new file mode 100644 index 0000000000..d0745f67d4 --- /dev/null +++ b/plugins/github-actions/src/components/Router.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { Routes, Route } from 'react-router'; +import { rootRouteRef, buildRouteRef } from '../plugin'; +import { WorkflowRunDetails } from './WorkflowRunDetails'; +import { WorkflowRunsTable } from './WorkflowRunsTable'; +import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName'; +import { WarningPanel } from '@backstage/core'; + +const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]) && + entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] !== ''; + +export const Router = ({ entity }: { entity: Entity }) => + // TODO(shmidt-i): move warning to a separate standardized component + !isPluginApplicableToEntity(entity) ? ( + + `entity.metadata.annotations[' + {GITHUB_ACTIONS_ANNOTATION}']` key is missing on the entity.{' '} + + ) : ( + + } + /> + } + /> + ) + + ); diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index 8c445ca9f6..d8e3b02524 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import React from 'react'; -import { useEntityCompoundName } from '@backstage/plugin-catalog'; import { useWorkflowRunsDetails } from './useWorkflowRunsDetails'; import { useWorkflowRunJobs } from './useWorkflowRunJobs'; import { useProjectName } from '../useProjectName'; @@ -35,13 +34,16 @@ import { LinearProgress, CircularProgress, Theme, - Link, + Breadcrumbs, + Link as MaterialLink, } from '@material-ui/core'; import { Jobs, Job, Step } from '../../api'; import moment from 'moment'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ExternalLinkIcon from '@material-ui/icons/Launch'; +import { Entity } from '@backstage/catalog-model'; +import { Link } from '@backstage/core'; const useStyles = makeStyles(theme => ({ root: { @@ -140,18 +142,8 @@ const JobListItem = ({ job, className }: { job: Job; className: string }) => { ); }; -export const WorkflowRunDetails = () => { - let entityCompoundName = useEntityCompoundName(); - if (!entityCompoundName.name) { - // TODO(shmidt-i): remove when is fully integrated - // into the entity view - entityCompoundName = { - kind: 'Component', - name: 'backstage', - namespace: 'default', - }; - } - const projectName = useProjectName(entityCompoundName); +export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { + const projectName = useProjectName(entity); const [owner, repo] = projectName.value ? projectName.value.split('/') : []; const details = useWorkflowRunsDetails(repo, owner); @@ -170,6 +162,10 @@ export const WorkflowRunDetails = () => { } return (
+ + Workflow runs + Workflow run details + @@ -211,10 +207,10 @@ export const WorkflowRunDetails = () => { {details.value?.html_url && ( - + Workflow runs on GitHub{' '} - + )} diff --git a/plugins/github-actions/src/components/WorkflowRunDetailsPage/WorkflowRunDetailsPage.tsx b/plugins/github-actions/src/components/WorkflowRunDetailsPage/WorkflowRunDetailsPage.tsx deleted file mode 100644 index ec9a3f484d..0000000000 --- a/plugins/github-actions/src/components/WorkflowRunDetailsPage/WorkflowRunDetailsPage.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Typography, Grid, Breadcrumbs } from '@material-ui/core'; - -import React from 'react'; -import { - Link, - Page, - Header, - HeaderLabel, - Content, - ContentHeader, - SupportButton, - pageTheme, -} from '@backstage/core'; - -import { WorkflowRunDetails } from '../WorkflowRunDetails'; - -/** - * A component for Jobs visualization. Jobs are a property of a Workflow Run. - */ -export const WorkflowRunDetailsPage = () => { - return ( - -
- - -
- - - - This plugin allows you to view and interact with your builds within - the GitHub Actions environment. - - - - Workflow runs - Workflow run details - - - - - - - -
- ); -}; diff --git a/plugins/github-actions/src/components/WorkflowRunsPage/WorkflowRunsPage.tsx b/plugins/github-actions/src/components/WorkflowRunsPage/WorkflowRunsPage.tsx deleted file mode 100644 index b75a70f326..0000000000 --- a/plugins/github-actions/src/components/WorkflowRunsPage/WorkflowRunsPage.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Header, - HeaderLabel, - pageTheme, - Page, - Content, - ContentHeader, - SupportButton, -} from '@backstage/core'; -import { Grid } from '@material-ui/core'; -import React from 'react'; - -import { WorkflowRunsTable } from '../WorkflowRunsTable'; - -export const WorkflowRunsPage = () => { - return ( - -
- - -
- - - - This plugin allows you to view and interact with your builds within - the GitHub Actions environment. - - - - - - - - -
- ); -}; diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index a2a6ead9b5..cc834f09ee 100644 --- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -23,8 +23,8 @@ import { useWorkflowRuns } from '../useWorkflowRuns'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import SyncIcon from '@material-ui/icons/Sync'; import { buildRouteRef } from '../../plugin'; -import { useEntityCompoundName } from '@backstage/plugin-catalog'; import { useProjectName } from '../useProjectName'; +import { Entity } from '@backstage/catalog-model'; export type WorkflowRun = { id: string; @@ -134,6 +134,7 @@ export const WorkflowRunsTableView: FC = ({ data={runs ?? []} onChangePage={onChangePage} onChangeRowsPerPage={onChangePageSize} + style={{ width: '100%' }} title={ @@ -146,25 +147,21 @@ export const WorkflowRunsTableView: FC = ({ ); }; -export const WorkflowRunsTable = () => { - let entityCompoundName = useEntityCompoundName(); - - if (!entityCompoundName.name) { - // TODO(shmidt-i): remove when is fully integrated - // into the entity view - entityCompoundName = { - kind: 'Component', - name: 'backstage', - namespace: 'default', - }; - } - - const { value: projectName, loading } = useProjectName(entityCompoundName); +export const WorkflowRunsTable = ({ + entity, + branch, +}: { + entity: Entity; + branch?: string; +}) => { + const { value: projectName, loading } = useProjectName(entity); const [owner, repo] = (projectName ?? '/').split('/'); const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({ owner, repo, + branch, }); + return ( { - const catalogApi = useApi(catalogApiRef); +export const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; +export const useProjectName = (entity: Entity) => { const { value, loading, error } = useAsync(async () => { - const entity = await catalogApi.getEntityByName(name); - return entity?.metadata.annotations?.['github.com/project-slug'] ?? ''; + return entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? ''; }); return { value, loading, error }; }; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 4a69c363cd..17c1fa2dd7 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -16,4 +16,6 @@ export { plugin } from './plugin'; export * from './api'; -export { Widget } from './components/Widget'; +export { Router } from './components/Router'; +export * from './components/Cards'; +export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index dbc366bd71..7e08229d6d 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -15,28 +15,18 @@ */ import { createPlugin, createRouteRef } from '@backstage/core'; -import { WorkflowRunDetailsPage } from './components/WorkflowRunDetailsPage'; -import { WorkflowRunsPage } from './components/WorkflowRunsPage'; // TODO(freben): This is just a demo route for now export const rootRouteRef = createRouteRef({ - path: '/github-actions', + path: '', title: 'GitHub Actions', }); -export const projectRouteRef = createRouteRef({ - path: '/github-actions/:kind/:optionalNamespaceAndName/', - title: 'GitHub Actions for project', -}); + export const buildRouteRef = createRouteRef({ - path: '/github-actions/workflow-run/:id', + path: ':id', title: 'GitHub Actions Workflow Run', }); export const plugin = createPlugin({ id: 'github-actions', - register({ router }) { - router.addRoute(rootRouteRef, WorkflowRunsPage); - router.addRoute(projectRouteRef, WorkflowRunsPage); - router.addRoute(buildRouteRef, WorkflowRunDetailsPage); - }, }); diff --git a/plugins/gitops-profiles/README.md b/plugins/gitops-profiles/README.md index d083c97b84..472ea3cc4e 100644 --- a/plugins/gitops-profiles/README.md +++ b/plugins/gitops-profiles/README.md @@ -11,7 +11,7 @@ Your plugin has been added to the example app in this repository, meaning you'll You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. ## Use GitOps-API backend with Backstage diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 5815386014..dc32a4df14 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx index 50268e6a50..06074ff84d 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx @@ -26,6 +26,7 @@ import { githubAuthApiRef, GithubAuth, OAuthRequestManager, + UrlPatternDiscovery, } from '@backstage/core'; import { gitOpsApiRef, GitOpsRestApi } from '../../api'; @@ -37,8 +38,9 @@ describe('ProfileCatalog', () => { [ githubAuthApiRef, GithubAuth.create({ - apiOrigin: 'http://localhost:7000', - basePath: '/auth/', + discoveryApi: UrlPatternDiscovery.compile( + 'http://example.com/{{pluginId}}', + ), oauthRequestApi, }), ], diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index a034c14a11..cc1a0a7204 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "private": false, "publishConfig": { "access": "public", @@ -31,25 +31,25 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "graphiql": "^1.0.0-alpha.10", - "graphql": "15.1.0", + "graphql": "15.3.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/codemirror": "^0.0.96", + "@types/codemirror": "^0.0.97", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", diff --git a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts index ee0b4bfd4e..23a9e9e697 100644 --- a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts +++ b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts @@ -33,7 +33,7 @@ export type GithubEndpointConfig = { id: string; title: string; /** - * Github GraphQL API url, defaults to https://api.github.com/graphql + * GitHub GraphQL API url, defaults to https://api.github.com/graphql */ url?: string; /** diff --git a/plugins/graphql/README.md b/plugins/graphql/README.md index 3f85ec766e..ae58396880 100644 --- a/plugins/graphql/README.md +++ b/plugins/graphql/README.md @@ -1,13 +1,28 @@ -# graphql +# GraphQL Backend -Welcome to the graphql backend plugin! +## Getting Started -_This plugin was created through the Backstage CLI_ +This backend plugin can be started in a standalone mode from directly in this package +with `yarn start`. However, it will have limited functionality and that process is +most convenient when developing the plugin itself. -## Getting started +To run it within the backend do: -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/graphql](http://localhost:3000/graphql). +1. Register the router in `packages/backend/src/index.ts`: -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +```ts +const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); + +const service = createServiceBuilder(module) + .loadConfig(configReader) + /** several different routers */ + .addRouter('/graphql', await graphql(graphqlEnv)); +``` + +2. Start the backend + +```bash +yarn workspace example-backend start +``` + +This will launch the full example backend. diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index 7a32883a44..1612398ea9 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-graphql-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,22 +20,22 @@ "mock-data": "./scripts/mock-data.sh" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", "@types/express": "^4.17.6", "apollo-server": "^2.16.0", "apollo-server-express": "^2.16.0", "express": "^4.17.1", "express-promise-router": "^3.0.3", "graphql": "^15.3.0", - "whatwg-fetch": "^2.0.0", + "whatwg-fetch": "^3.4.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.19.5", + "msw": "^0.20.5", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/graphql/src/service/router.ts b/plugins/graphql/src/service/router.ts index 69e2394f11..df5185c1b1 100644 --- a/plugins/graphql/src/service/router.ts +++ b/plugins/graphql/src/service/router.ts @@ -14,17 +14,16 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { errorHandler, resolvePackagePath } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import fs from 'fs'; -import path from 'path'; import { ApolloServer } from 'apollo-server-express'; -const schemaPath = path.resolve( - require.resolve('@backstage/plugin-graphql-backend/package.json'), - '../schema.gql', +const schemaPath = resolvePackagePath( + '@backstage/plugin-graphql-backend', + 'schema.gql', ); export interface RouterOptions { @@ -39,8 +38,8 @@ export async function createRouter( const server = new ApolloServer({ typeDefs, logger: options.logger }); const router = Router(); - const apolloMiddlware = server.getMiddleware({ path: '/' }); - router.use(apolloMiddlware); + const apolloMiddleware = server.getMiddleware({ path: '/' }); + router.use(apolloMiddleware); router.get('/health', (_, response) => { response.send({ status: 'ok' }); diff --git a/plugins/identity-backend/README.md b/plugins/identity-backend/README.md index 7d97bb5f6b..c95fed0698 100644 --- a/plugins/identity-backend/README.md +++ b/plugins/identity-backend/README.md @@ -8,4 +8,4 @@ It responds to identity requests from the frontend. ## Links -- (The Backstage homepage)[https://backstage.io] +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index c5c3bffc9b..18db7c9220 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-identity-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", @@ -33,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/identity-backend/src/service/router.ts b/plugins/identity-backend/src/service/router.ts index 9c6515ac04..9d16e8f332 100644 --- a/plugins/identity-backend/src/service/router.ts +++ b/plugins/identity-backend/src/service/router.ts @@ -27,13 +27,15 @@ export interface RouterOptions { const makeRouter = (adapter: IdentityApi): express.Router => { const router = Router(); + router.use(express.json()); + router.get('/users/:user/groups', async (req, res) => { const user = req.params.user; const type = req.query.type?.toString() ?? ''; - const response = await adapter.getUserGroups({ user, type }); res.send(response); }); + return router; }; diff --git a/plugins/jenkins/.eslintrc.js b/plugins/jenkins/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/jenkins/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md new file mode 100644 index 0000000000..a911704dc4 --- /dev/null +++ b/plugins/jenkins/README.md @@ -0,0 +1,87 @@ +# Jenkins Plugin (Alpha) + +Website: [https://jenkins.io/](https://jenkins.io/) + +Last master build +Folder results +Build details + +## Setup + +1. If you have a standalone app (you didn't clone this repo), then do + +```bash +yarn add @backstage/plugin-jenkins +``` + +2. Add plugin API to your Backstage instance: + +```js +// packages/app/src/api.ts +import { JenkinsApi, jenkinsApiRef } from '@backstage/plugin-jenkins'; + +const builder = ApiRegistry.builder(); +builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`)); +``` + +2. Add plugin itself: + +```js +// packages/app/src/plugins.ts +export { plugin as Jenkins } from '@backstage/plugin-jenkins'; +``` + +3. Add proxy configuration to `app-config.yaml` + +```yaml +proxy: + '/jenkins/api': + target: 'http://localhost:8080' # your Jenkins URL + changeOrigin: true + headers: + Authorization: + $secret: + env: JENKINS_BASIC_AUTH_HEADER + pathRewrite: + '^/proxy/jenkins/api/': '/' +``` + +4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password) + +```shell +HEADER=$(echo -n user:api-token | base64) +export JENKINS_BASIC_AUTH_HEADER="Basic $HEADER" +``` + +5. Run app with `yarn start` +6. Add the Jenkins folder annotation to your `component-info.yaml`, (note: currently this plugin only supports folders and Git SCM) + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: 'your-component' + description: 'a description' + annotations: + backstage.io/jenkins-github-folder: 'folder-name/job-name' +spec: + type: service + lifecycle: experimental + owner: your-name +``` + +7. Register your component + +8. Click the component in the catalog you should now see Jenkins builds, and a last build result for your master build. + +## Features + +- View all runs inside a folder +- Last build status for specified branch +- View summary of a build + +## Limitations + +- Only works with projects that use the Git SCM +- It requires jobs to be organised into folders +- No pagination support currently - don't run this on a Jenkins with lots of builds diff --git a/plugins/jenkins/dev/index.tsx b/plugins/jenkins/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/jenkins/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json new file mode 100644 index 0000000000..da1754749f --- /dev/null +++ b/plugins/jenkins/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-jenkins", + "version": "0.1.1-alpha.21", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "jenkins": "^0.28.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "^5.9.1", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts new file mode 100644 index 0000000000..545a1da9cc --- /dev/null +++ b/plugins/jenkins/src/api/index.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core'; +import { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable'; + +const jenkins = require('jenkins'); + +export const jenkinsApiRef = createApiRef({ + id: 'plugin.jenkins.service', + description: 'Used by the Jenkins plugin to make requests', +}); + +export class JenkinsApi { + apiUrl: string; + jenkins: any; + + constructor(apiUrl: string) { + this.apiUrl = apiUrl; + this.jenkins = jenkins({ baseUrl: apiUrl, promisify: true }); + } + + async retry(buildName: string) { + // looks like the current SDK only supports triggering a new build + // can't see any support for replay (re-running the specific build with the same SCM info) + return await this.jenkins.job.build(buildName); + } + + async getLastBuild(jobName: string) { + const job = await this.jenkins.job.get(jobName); + + const lastBuild = await this.jenkins.build.get( + jobName, + job.lastBuild.number, + ); + return lastBuild; + } + + extractScmDetailsFromJob(jobDetails: any): any { + const scmInfo = jobDetails.actions + .filter( + (action: any) => + action._class === 'jenkins.scm.api.metadata.ObjectMetadataAction', + ) + .map((action: any) => { + return { + url: action?.objectUrl, + // https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/metadata/ObjectMetadataAction.html + // branch name for regular builds, pull request title on pull requests + displayName: action?.objectDisplayName, + }; + }) + .pop(); + + return scmInfo; + } + + async getJob(jobName: string) { + return this.jenkins.job.get({ + name: jobName, + depth: 1, + }); + } + + async getFolder(folderName: string) { + const folder = await this.jenkins.job.get(folderName); + const results = []; + for (const jobSummary of folder.jobs) { + const jobDetails = await this.jenkins.job.get({ + name: `${folderName}/${jobSummary.name}`, + depth: 1, + }); + + const jobScmInfo = this.extractScmDetailsFromJob(jobDetails); + if (jobDetails.jobs) { + // skipping folders inside folders for now + } else { + for (const buildDetails of jobDetails.builds) { + const build = await this.jenkins.build.get({ + name: `${folderName}/${jobSummary.name}`, + number: buildDetails.number, + depth: 1, + }); + + const ciTable = this.mapJenkinsBuildToCITable(build, jobScmInfo); + results.push(ciTable); + } + } + } + return results; + } + + private getTestReport( + jenkinsResult: any, + ): { + total: number; + passed: number; + skipped: number; + failed: number; + testUrl: string; + } { + return jenkinsResult.actions + .filter( + (action: any) => + action._class === 'hudson.tasks.junit.TestResultAction', + ) + .map((action: any) => { + return { + total: action.totalCount, + passed: action.totalCount - action.failCount - action.skipCount, + skipped: action.skipCount, + failed: action.failCount, + testUrl: `${jenkinsResult.url}${action.urlName}/`, + }; + }) + .pop(); + } + + mapJenkinsBuildToCITable( + jenkinsResult: any, + jobScmInfo?: any, + ): CITableBuildInfo { + const source = + jenkinsResult.actions + .filter( + (action: any) => + action._class === 'hudson.plugins.git.util.BuildData', + ) + .map((action: any) => { + const [first]: any = Object.values(action.buildsByBranchName); + const branch = first.revision.branch[0]; + return { + branchName: branch.name, + commit: { + hash: branch.SHA1.substring(0, 8), + }, + }; + }) + .pop() || {}; + + if (jobScmInfo) { + source.url = jobScmInfo?.url; + source.displayName = jobScmInfo?.displayName; + } + + const path = new URL(jenkinsResult.url).pathname; + + return { + id: path, + buildName: jenkinsResult.fullDisplayName, + status: jenkinsResult.building ? 'running' : jenkinsResult.result, + onRestartClick: () => { + // TODO: this won't handle non root context path, need a better way to get the job name + const { jobName } = this.extractJobDetailsFromBuildName(path); + return this.retry(jobName); + }, + source: source, + tests: this.getTestReport(jenkinsResult), + }; + } + + async getBuild(buildName: string) { + const { jobName, buildNumber } = this.extractJobDetailsFromBuildName( + buildName, + ); + const buildResult = await this.jenkins.build.get(jobName, buildNumber); + return buildResult; + } + + extractJobDetailsFromBuildName(buildName: string) { + const trimmedBuild = buildName.replace(/\/job/g, '').replace(/\/$/, ''); + + const split = trimmedBuild.split('/'); + const buildNumber = parseInt(split[split.length - 1], 10); + const jobName = trimmedBuild.slice( + 0, + trimmedBuild.length - buildNumber.toString(10).length - 1, + ); + + return { + jobName, + buildNumber, + }; + } +} diff --git a/plugins/jenkins/src/assets/build-details.png b/plugins/jenkins/src/assets/build-details.png new file mode 100644 index 0000000000..396af428af Binary files /dev/null and b/plugins/jenkins/src/assets/build-details.png differ diff --git a/plugins/jenkins/src/assets/folder-results.png b/plugins/jenkins/src/assets/folder-results.png new file mode 100644 index 0000000000..2e14f3551f Binary files /dev/null and b/plugins/jenkins/src/assets/folder-results.png differ diff --git a/plugins/jenkins/src/assets/last-master-build.png b/plugins/jenkins/src/assets/last-master-build.png new file mode 100644 index 0000000000..517e7a4410 Binary files /dev/null and b/plugins/jenkins/src/assets/last-master-build.png differ diff --git a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx similarity index 66% rename from plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx rename to plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx index 57082a9c91..469869af69 100644 --- a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx @@ -14,16 +14,14 @@ * limitations under the License. */ +import React from 'react'; +import { Builds } from '../../pages/BuildsPage/lib/Builds'; import { Entity } from '@backstage/catalog-model'; -import { InfoCard, StructuredMetadataTable } from '@backstage/core'; -import React, { FC } from 'react'; -type Props = { - entity: Entity; +export const JenkinsBuildsWidget = ({ entity }: { entity: Entity }) => { + const [owner, repo] = ( + entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/' + ).split('/'); + + return ; }; - -export const EntityMetadataCard: FC = ({ entity }) => ( - - - -); diff --git a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx new file mode 100644 index 0000000000..9bd16bf9e6 --- /dev/null +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; +import { InfoCard, StructuredMetadataTable } from '@backstage/core'; +import ExternalLinkIcon from '@material-ui/icons/Launch'; +import { useBuilds } from '../../state'; +import { JenkinsRunStatus } from '../../pages/BuildsPage/lib/Status'; + +const useStyles = makeStyles({ + externalLinkIcon: { + fontSize: 'inherit', + verticalAlign: 'bottom', + }, +}); + +const WidgetContent = ({ + loading, + lastRun, +}: { + loading?: boolean; + lastRun: any; + branch: string; +}) => { + const classes = useStyles(); + if (loading || !lastRun) return ; + return ( + + + + ), + build: lastRun.fullDisplayName, + url: ( + + See more on Jenkins{' '} + + + ), + }} + /> + ); +}; + +export const JenkinsLastBuildWidget = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => { + const [owner, repo] = ( + entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/' + ).split('/'); + const [{ loading, value }] = useBuilds(owner, repo, branch); + + const lastRun = value ?? {}; + + return ( + + + + ); +}; diff --git a/plugins/jenkins/src/components/Layout/Layout.tsx b/plugins/jenkins/src/components/Layout/Layout.tsx new file mode 100644 index 0000000000..c6000437a3 --- /dev/null +++ b/plugins/jenkins/src/components/Layout/Layout.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; + +export const Layout: React.FC = ({ children }) => { + return ( + +
+ + +
+ {children} +
+ ); +}; diff --git a/plugins/jenkins/src/components/Layout/index.ts b/plugins/jenkins/src/components/Layout/index.ts new file mode 100644 index 0000000000..236fc98851 --- /dev/null +++ b/plugins/jenkins/src/components/Layout/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './Layout'; diff --git a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx new file mode 100644 index 0000000000..501649e49a --- /dev/null +++ b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ContentHeader, SupportButton } from '@backstage/core'; +import { Box, Typography } from '@material-ui/core'; + +export type Props = { title?: string }; +export const PluginHeader = ({ title = 'Jenkins' }) => { + return ( + ( + + {title} + + )} + > + + This plugin allows you to view and interact with your builds in Jenkins. + + + ); +}; diff --git a/plugins/jenkins/src/components/PluginHeader/index.ts b/plugins/jenkins/src/components/PluginHeader/index.ts new file mode 100644 index 0000000000..4de972f6f2 --- /dev/null +++ b/plugins/jenkins/src/components/PluginHeader/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './PluginHeader'; diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts new file mode 100644 index 0000000000..c7f2d8dc28 --- /dev/null +++ b/plugins/jenkins/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { plugin, JenkinsBuildsWidget, JenkinsLastBuildWidget } from './plugin'; +export * from './api'; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx new file mode 100644 index 0000000000..ab4fc3f34f --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -0,0 +1,164 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { FC, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Content, InfoCard, Progress } from '@backstage/core'; +import { Grid, Box, Link, IconButton } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { PluginHeader } from '../../components/PluginHeader'; +import { ActionOutput } from './lib/ActionOutput/ActionOutput'; +import { Layout } from '../../components/Layout'; +import LaunchIcon from '@material-ui/icons/Launch'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import { useBuildWithSteps } from '../../state/useBuildWithSteps'; + +const IconLink = IconButton as typeof Link; +const BuildName: FC<{ build?: any }> = ({ build }) => ( + + {build?.buildName} + + {/* TODO use Jenkins logo*/} + + + + + +); +const useStyles = makeStyles(theme => ({ + neutral: {}, + failed: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`, + }, + }, + running: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`, + }, + }, + cardContent: { + backgroundColor: theme.palette.background.default, + }, + success: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`, + }, + }, +})); + +const pickClassName = ( + classes: ReturnType, + build: any = {}, +) => { + if (build.result === 'UNSTABLE') return classes.failed; + if (build.result === 'FAILURE') return classes.failed; + if (build.building) return classes.running; + if (build.status === 'SUCCESS') return classes.success; + + return classes.neutral; +}; + +const Page = () => ( + + + + + +); + +const BuildWithStepsView = () => { + const [searchParams] = useSearchParams(); + const buildPath = searchParams.get('url') || ''; + const classes = useStyles(); + const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps( + buildPath, + ); + + useEffect(() => { + startPolling(); + return () => stopPolling(); + }, [buildPath, startPolling, stopPolling]); + return ( + <> + + + + + } + cardClassName={classes.cardContent} + > + {loading ? : } + + + + + ); +}; + +const BuildsList: FC<{ build?: any }> = ({ build }) => ( + + {build && + build.steps && + build.steps.map(({ name, actions }: { name: string; actions: any[] }) => ( + + ))} + +); + +const ActionsList: FC<{ actions: any[]; name: string }> = ({ actions }) => { + const classes = useStyles(); + return ( + <> + {actions.map((action: any) => ( + + ))} + + ); +}; + +export default Page; +export { BuildWithStepsView as BuildWithSteps }; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts b/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts new file mode 100644 index 0000000000..fddff7088c --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + default as DetailedViewPage, + BuildWithSteps, +} from './BuildWithStepsPage'; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx new file mode 100644 index 0000000000..1143121a98 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useEffect, FC } from 'react'; +import { + ExpansionPanel, + ExpansionPanelSummary, + Typography, + ExpansionPanelDetails, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + expansionPanelDetails: { + padding: 0, + }, + button: { + order: -1, + marginRight: 0, + marginLeft: '-20px', + }, +}); + +export const ActionOutput: FC<{ + url: string; + name: string; + className?: string; + action: any; +}> = ({ url, name, className }) => { + const classes = useStyles(); + + useEffect(() => {}, [url]); + + return ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + {name} + + + Nothing here... + + + ); +}; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts new file mode 100644 index 0000000000..7cf74c73f8 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ActionOutput } from './ActionOutput'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx new file mode 100644 index 0000000000..79f081c82d --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { CITable } from '../CITable'; +import { useBuilds } from '../../../../state'; + +export const Builds = ({ owner, repo }: { owner: string; repo: string }) => { + const [ + { total, loading, value, projectName, page, pageSize }, + { setPage, retry, setPageSize }, + ] = useBuilds(owner, repo); + return ( + + ); +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts new file mode 100644 index 0000000000..e91a9496b7 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { Builds } from './Builds'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx new file mode 100644 index 0000000000..7f17107374 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -0,0 +1,218 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { FC } from 'react'; +import { Link, Typography, Box, IconButton } from '@material-ui/core'; +import RetryIcon from '@material-ui/icons/Replay'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import { Link as RouterLink } from 'react-router-dom'; +import { Table, TableColumn } from '@backstage/core'; +import { JenkinsRunStatus } from '../Status'; + +export type CITableBuildInfo = { + id: string; + buildName: string; + buildUrl?: string; + source: { + branchName: string; + url: string; + displayName: string; + commit: { + hash: string; + }; + }; + status: string; + tests?: { + total: number; + passed: number; + skipped: number; + failed: number; + testUrl: string; + }; + onRestartClick: () => void; +}; + +const FailCount = ({ count }: { count: number }): JSX.Element | null => { + if (count !== 0) { + return <>{count} failed; + } + return null; +}; + +const SkippedCount = ({ count }: { count: number }): JSX.Element | null => { + if (count !== 0) { + return <>{count} skipped; + } + return null; +}; + +const FailSkippedWidget = ({ + skipped, + failed, +}: { + skipped: number; + failed: number; +}): JSX.Element | null => { + if (skipped === 0 && failed === 0) { + return null; + } + + if (skipped !== 0 && failed !== 0) { + return ( + <> + {' '} + (, ) + + ); + } + + if (failed !== 0) { + return ( + <> + {' '} + () + + ); + } + + if (skipped !== 0) { + return ( + <> + {' '} + () + + ); + } + + return null; +}; + +const generatedColumns: TableColumn[] = [ + { + title: 'Build', + field: 'buildName', + highlight: true, + render: (row: Partial) => ( + + {row.buildName} + + ), + }, + { + title: 'Source', + render: (row: Partial) => ( + <> +

+ + {row.source?.branchName} + +

+

{row.source?.commit?.hash}

+ + ), + }, + { + title: 'Status', + render: (row: Partial) => { + return ( + + + + ); + }, + }, + { + title: 'Tests', + render: (row: Partial) => { + return ( + <> +

+ {row.tests && ( + + {row.tests.passed} / {row.tests.total} passed + + + )} + + {!row.tests && 'n/a'} +

+ + ); + }, + }, + { + title: 'Actions', + render: (row: Partial) => ( + + + + ), + width: '10%', + }, +]; + +type Props = { + loading: boolean; + retry: () => void; + builds: CITableBuildInfo[]; + projectName: string; + page: number; + onChangePage: (page: number) => void; + total: number; + pageSize: number; + onChangePageSize: (pageSize: number) => void; +}; +export const CITable: FC = ({ + projectName, + loading, + pageSize, + page, + retry, + builds, + onChangePage, + onChangePageSize, + total, +}) => { + return ( +
, + tooltip: 'Refresh Data', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={builds} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} + title={ + + + + {projectName} + + } + columns={generatedColumns} + /> + ); +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts new file mode 100644 index 0000000000..358939e69f --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { CITable } from './CITable'; +export type { CITableBuildInfo } from './CITable'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx new file mode 100644 index 0000000000..66a58abe76 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + StatusPending, + StatusRunning, + StatusOK, + StatusWarning, + StatusAborted, + StatusError, +} from '@backstage/core'; +import React from 'react'; + +export const JenkinsRunStatus = ({ + status, +}: { + status: string | undefined; +}) => { + if (status === undefined) return null; + switch (status.toLowerCase()) { + case 'queued': + case 'scheduled': + return ( + <> + Queued + + ); + case 'running': + return ( + <> + In progress + + ); + case 'unstable': + return ( + <> + Unstable + + ); + case 'failure': + return ( + <> + Failed + + ); + case 'success': + return ( + <> + Completed + + ); + case 'aborted': + return ( + <> + Aborted + + ); + default: + return ( + <> + {status} + + ); + } +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts new file mode 100644 index 0000000000..f5dd4a55e5 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { JenkinsRunStatus } from './JenkinsRunStatus'; diff --git a/plugins/jenkins/src/plugin.test.ts b/plugins/jenkins/src/plugin.test.ts new file mode 100644 index 0000000000..8c3f057ecb --- /dev/null +++ b/plugins/jenkins/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { plugin } from './plugin'; + +describe('jenkins', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts new file mode 100644 index 0000000000..979b0a31cf --- /dev/null +++ b/plugins/jenkins/src/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createPlugin, createRouteRef } from '@backstage/core'; +import { DetailedViewPage } from './pages/BuildWithStepsPage'; + +export const buildRouteRef = createRouteRef({ + path: '/jenkins/job', + title: 'Jenkins run', +}); + +export const plugin = createPlugin({ + id: 'jenkins', + register({ router }) { + router.addRoute(buildRouteRef, DetailedViewPage); + }, +}); + +export { JenkinsBuildsWidget } from './components/JenkinsPluginWidget/JenkinsBuildsWidget'; +export { JenkinsLastBuildWidget } from './components/JenkinsPluginWidget/JenkinsLastBuildWidget'; diff --git a/plugins/jenkins/src/setupTests.ts b/plugins/jenkins/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/jenkins/src/setupTests.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; + +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/jenkins/src/state/index.ts b/plugins/jenkins/src/state/index.ts new file mode 100644 index 0000000000..d21a380c2a --- /dev/null +++ b/plugins/jenkins/src/state/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './useBuilds'; +export * from './useBuildWithSteps'; diff --git a/plugins/jenkins/src/state/useAsyncPolling.ts b/plugins/jenkins/src/state/useAsyncPolling.ts new file mode 100644 index 0000000000..7ea0755368 --- /dev/null +++ b/plugins/jenkins/src/state/useAsyncPolling.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useRef } from 'react'; + +export const useAsyncPolling = ( + pollingFn: () => Promise, + interval: number, +) => { + const isPolling = useRef(false); + const startPolling = async () => { + if (isPolling.current === true) return; + isPolling.current = true; + + while (isPolling.current === true) { + await pollingFn(); + await new Promise(resolve => setTimeout(resolve, interval)); + } + }; + + const stopPolling = () => { + isPolling.current = false; + }; + return { startPolling, stopPolling }; +}; diff --git a/plugins/jenkins/src/state/useBuildWithSteps.ts b/plugins/jenkins/src/state/useBuildWithSteps.ts new file mode 100644 index 0000000000..617b2b1f4f --- /dev/null +++ b/plugins/jenkins/src/state/useBuildWithSteps.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { errorApiRef, useApi } from '@backstage/core'; +import { useCallback } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { jenkinsApiRef } from '../api/index'; +import { useAsyncPolling } from './useAsyncPolling'; + +const INTERVAL_AMOUNT = 1500; +export function useBuildWithSteps(buildName: string) { + const api = useApi(jenkinsApiRef); + const errorApi = useApi(errorApiRef); + + const getBuildWithSteps = useCallback(async () => { + try { + const build = await api.getBuild(buildName); + + const { jobName } = api.extractJobDetailsFromBuildName(buildName); + const job = await api.getJob(jobName); + const jobInfo = api.extractScmDetailsFromJob(job); + + return Promise.resolve(api.mapJenkinsBuildToCITable(build, jobInfo)); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, [buildName, api, errorApi]); + + const restartBuild = async () => { + try { + await api.retry(buildName); + } catch (e) { + errorApi.post(e); + } + }; + + const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [ + getBuildWithSteps, + ]); + + const { startPolling, stopPolling } = useAsyncPolling( + getBuildWithSteps, + INTERVAL_AMOUNT, + ); + + return [ + { loading, value, retry }, + { + restartBuild, + getBuildWithSteps, + startPolling, + stopPolling, + }, + ] as const; +} diff --git a/plugins/jenkins/src/state/useBuilds.ts b/plugins/jenkins/src/state/useBuilds.ts new file mode 100644 index 0000000000..deb3125637 --- /dev/null +++ b/plugins/jenkins/src/state/useBuilds.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { errorApiRef, useApi } from '@backstage/core'; +import { useCallback, useEffect, useState } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { jenkinsApiRef } from '../api'; + +export function useBuilds(owner: string, repo: string, branch?: string) { + const api = useApi(jenkinsApiRef); + const errorApi = useApi(errorApiRef); + + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(5); + + const getBuilds = useCallback(async () => { + try { + let build; + if (branch) { + build = await api.getLastBuild(`${owner}/${repo}/${branch}`); + } else { + build = await api.getFolder(`${owner}/${repo}`); + } + return build; + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, [api, branch, errorApi, owner, repo]); + + const restartBuild = async (buildName: string) => { + try { + await api.retry(buildName); + } catch (e) { + errorApi.post(e); + } + }; + + useEffect(() => { + getBuilds().then(b => { + const size = Array.isArray(b) ? b?.[0].build_num! : 1; + setTotal(size); + }); + }, [repo, getBuilds]); + + const { loading, value, retry } = useAsyncRetry( + () => getBuilds().then(builds => builds ?? [], restartBuild), + [page, pageSize, getBuilds], + ); + + const projectName = `${owner}/${repo}`; + return [ + { + page, + pageSize, + loading, + value, + projectName, + total, + }, + { + getBuilds, + setPage, + setPageSize, + restartBuild, + retry, + }, + ] as const; +} diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md index 337e0f9533..b5fecb85f8 100644 --- a/plugins/lighthouse/README.md +++ b/plugins/lighthouse/README.md @@ -36,16 +36,24 @@ your [`apis.ts`](https://github.com/spotify/backstage/blob/master/packages/app/s ```js import { ApiHolder, ApiRegistry } from '@backstage/core'; +import { Config } from '@backstage/config'; import { lighthouseApiRef, LighthouseRestApi, } from '@backstage/plugin-lighthouse'; -const builder = ApiRegistry.builder(); +export const apis = (config: ConfigApi) => { + const builder = ApiRegistry.builder(); -export const lighthouseApi = - new LighthouseRestApi(/* your service url here! */); -builder.add(lighthouseApiRef, lighthouseApi); + builder.add(lighthouseApiRef, LighthouseRestApi.fromConfig(config)); -export default builder.build() as ApiHolder; + return builder.build() as ApiHolder; +} +``` + +Then configure the lighthouse service url in your [`app-config.yaml`](https://github.com/spotify/backstage/blob/master/app-config.yaml). + +```yaml +lighthouse: + baseUrl: http://your-service-url ``` diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index cbf4a77f7e..647215f7f8 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/config": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,9 +34,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts index 9c656c88ef..5b6cd9aae7 100644 --- a/plugins/lighthouse/src/api.ts +++ b/plugins/lighthouse/src/api.ts @@ -15,6 +15,7 @@ */ import { createApiRef } from '@backstage/core'; +import { Config } from '@backstage/config'; export type LighthouseCategoryId = | 'pwa' @@ -111,6 +112,10 @@ export const lighthouseApiRef = createApiRef({ }); export class LighthouseRestApi implements LighthouseApi { + static fromConfig(config: Config) { + return new LighthouseRestApi(config.getString('lighthouse.baseUrl')); + } + constructor(public url: string) {} private async fetch(input: string, init?: RequestInit): Promise { diff --git a/plugins/newrelic/.eslintrc.js b/plugins/newrelic/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/newrelic/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md new file mode 100644 index 0000000000..19bd24950c --- /dev/null +++ b/plugins/newrelic/README.md @@ -0,0 +1,39 @@ +# New Relic Plugin (Alpha) + +Website: [https://newrelic.com](https://newrelic.com) + +New Relic Plugin APM +New Relic Plugin Tools + +## Getting Started + +Add New Relic REST API Key to `app-config.yaml` + +```yaml +newrelic: + api: + baseUrl: 'https://api.newrelic.com/v2' + key: +``` + +New Relic Plugin Path: [/newrelic](http://localhost:3000/newrelic) + +## Features + +- View New Relic Application Performance Monitoring (APM) data such as: + - Application Name + - Response Time (ms) + - Throughput (rpm) + - Error Rate + - Instance Count + - Apdex Score + +## Limitations + +- Currently only supports New Relic APM data + +--- + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/newrelic/dev/index.tsx b/plugins/newrelic/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/newrelic/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/packages/create-app/templates/default-app/plugins/welcome/package.json.hbs b/plugins/newrelic/package.json similarity index 63% rename from packages/create-app/templates/default-app/plugins/welcome/package.json.hbs rename to plugins/newrelic/package.json index 77e57c1a7c..84f066edae 100644 --- a/packages/create-app/templates/default-app/plugins/welcome/package.json.hbs +++ b/plugins/newrelic/package.json @@ -1,10 +1,12 @@ { - "name": "plugin-welcome", - "version": "0.0.0", + "name": "@backstage/plugin-newrelic", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", + "license": "Apache-2.0", "private": true, "publishConfig": { + "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, @@ -19,20 +21,23 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^{{version}}", - "@backstage/theme": "^{{version}}", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3", - "react-router-dom": "6.0.0-beta.0" + "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^{{version}}", - "@backstage/dev-utils": "^{{version}}", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png b/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png new file mode 100644 index 0000000000..cc63306bba Binary files /dev/null and b/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png differ diff --git a/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png b/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png new file mode 100644 index 0000000000..e42013f14c Binary files /dev/null and b/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png differ diff --git a/plugins/newrelic/src/assets/img/newrelic.jpg b/plugins/newrelic/src/assets/img/newrelic.jpg new file mode 100644 index 0000000000..d4b3a0e196 Binary files /dev/null and b/plugins/newrelic/src/assets/img/newrelic.jpg differ diff --git a/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx b/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx new file mode 100644 index 0000000000..94d5c530a1 --- /dev/null +++ b/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { Grid } from '@material-ui/core'; +import { + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; +import NewRelicFetchComponent from '../NewRelicFetchComponent'; + +const NewRelicComponent: FC<{}> = () => ( + +
+ +
+ + + + New Relic Application Performance Monitoring + + + + + + + + +
+); + +export default NewRelicComponent; diff --git a/plugins/newrelic/src/components/NewRelicComponent/index.ts b/plugins/newrelic/src/components/NewRelicComponent/index.ts new file mode 100644 index 0000000000..de10a42770 --- /dev/null +++ b/plugins/newrelic/src/components/NewRelicComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './NewRelicComponent'; diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx new file mode 100644 index 0000000000..9b3791e062 --- /dev/null +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { + configApiRef, + Progress, + Table, + TableColumn, + useApi, +} from '@backstage/core'; +import Alert from '@material-ui/lab/Alert'; +import { useAsync } from 'react-use'; + +type NewRelicApplication = { + id: number; + application_summary: NewRelicApplicationSummary; + name: string; + language: string; + health_status: string; + reporting: boolean; + settings: NewRelicApplicationSettings; + links?: NewRelicApplicationLinks; +}; + +type NewRelicApplicationSummary = { + apdex_score: number; + error_rate: number; + host_count: number; + instance_count: number; + response_time: number; + throughput: number; +}; + +type NewRelicApplicationSettings = { + app_apdex_threshold: number; + end_user_apdex_threshold: number; + enable_real_user_monitoring: boolean; + use_server_side_config: boolean; +}; + +type NewRelicApplicationLinks = { + application_instances: Array; + servers: Array; + application_hosts: Array; +}; + +type NewRelicApplications = { + applications: NewRelicApplication[]; +}; + +export const NewRelicAPMTable: FC = ({ + applications, +}) => { + const columns: TableColumn[] = [ + { title: 'Application', field: 'name' }, + { title: 'Response Time', field: 'responseTime' }, + { title: 'Throughput', field: 'throughput' }, + { title: 'Error Rate', field: 'errorRate' }, + { title: 'Instance Count', field: 'instanceCount' }, + { title: 'Apdex', field: 'apdexScore' }, + ]; + const data = applications.map((app: NewRelicApplication) => { + const { name, application_summary: applicationSummary } = app; + const { + response_time: responseTime, + throughput, + error_rate: errorRate, + instance_count: instanceCount, + apdex_score: apdexScore, + } = applicationSummary; + + return { + name, + responseTime: `${responseTime} ms`, + throughput: `${throughput} rpm`, + errorRate: `${errorRate}%`, + instanceCount, + apdexScore, + }; + }); + + return ( +
+ ); +}; + +const NewRelicFetchComponent: FC<{}> = () => { + const configApi = useApi(configApiRef); + const apiBaseUrl = configApi.getString('newrelic.api.baseUrl'); + const apiKey = configApi.getString('newrelic.api.key'); + + const { value, loading, error } = useAsync(async (): Promise< + NewRelicApplication[] + > => { + const response = await fetch(`${apiBaseUrl}/applications.json`, { + headers: { + 'X-Api-Key': apiKey, + }, + }); + const data: NewRelicApplications = await response.json(); + return data.applications.filter((application: NewRelicApplication) => { + return application.hasOwnProperty('application_summary'); + }); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ; +}; + +export default NewRelicFetchComponent; diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/index.ts b/plugins/newrelic/src/components/NewRelicFetchComponent/index.ts new file mode 100644 index 0000000000..1907c0bf1d --- /dev/null +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './NewRelicFetchComponent'; diff --git a/plugins/newrelic/src/index.ts b/plugins/newrelic/src/index.ts new file mode 100644 index 0000000000..3a0a0fe2d3 --- /dev/null +++ b/plugins/newrelic/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { plugin } from './plugin'; diff --git a/plugins/newrelic/src/plugin.test.ts b/plugins/newrelic/src/plugin.test.ts new file mode 100644 index 0000000000..17429a95e0 --- /dev/null +++ b/plugins/newrelic/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { plugin } from './plugin'; + +describe('newrelic', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/packages/core/src/layout/Header/Waves.test.tsx b/plugins/newrelic/src/plugin.ts similarity index 62% rename from packages/core/src/layout/Header/Waves.test.tsx rename to plugins/newrelic/src/plugin.ts index 76bf5e913e..21aac69e8f 100644 --- a/packages/core/src/layout/Header/Waves.test.tsx +++ b/plugins/newrelic/src/plugin.ts @@ -14,14 +14,17 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; -import { pageTheme } from '../Page/PageThemeProvider'; -import { Waves } from './Waves'; +import { createPlugin, createRouteRef } from '@backstage/core'; +import NewRelicComponent from './components/NewRelicComponent'; -describe('', () => { - it('should render svg', () => { - const rendered = render(); - rendered.getByTestId('wave-svg'); - }); +export const rootRouteRef = createRouteRef({ + path: '/newrelic', + title: 'newrelic', +}); + +export const plugin = createPlugin({ + id: 'newrelic', + register({ router }) { + router.addRoute(rootRouteRef, NewRelicComponent); + }, }); diff --git a/plugins/newrelic/src/setupTests.ts b/plugins/newrelic/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/newrelic/src/setupTests.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; + +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/proxy-backend/README.md b/plugins/proxy-backend/README.md index d12031f495..f67449dda3 100644 --- a/plugins/proxy-backend/README.md +++ b/plugins/proxy-backend/README.md @@ -1,6 +1,7 @@ # Proxy backend plugin -This is the backend plugin that enables proxy definitions to be declared in and read from app-config.yaml. +This is the backend plugin that enables proxy definitions to be declared in, +and read from, `app-config.yaml`. Relies on the `http-proxy-middleware` package. @@ -10,27 +11,22 @@ This backend plugin can be started in a standalone mode from directly in this pa with `yarn start`. However, it will have limited functionality and that process is most convenient when developing the plugin itself. -To run it within the backend do: - -1. Register the router in `packages/backend/src/index.ts`: - -```ts -const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); - -const service = createServiceBuilder(module) - .loadConfig(configReader) - /** several different routers */ - .addRouter('/', await proxy(proxyEnv)); -``` - -2. Start the backend +The proxy is already installed in the Backstage backend per default, so you can also +start up the full example backend to experiment with the proxy. ```bash yarn workspace example-backend start ``` -This will launch the full example backend. +## Configuration + +See [the proxy docs](https://backstage.io/docs/plugins/proxying). ## Links +- [Call Existing API](https://backstage.io/docs/plugins/call-existing-api) helps the + decision process of what method of communication to use from a frontend plugin to + your API +- [The proxy plugin documentation](https://backstage.io/docs/plugins/proxying) describes + configuration options and more - [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware) diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 023fc81cf2..9f3bc16103 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,8 +19,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", - "@backstage/config": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.21", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -34,7 +34,7 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 085647bc4e..c7d7fff1d4 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -26,6 +26,7 @@ describe('createRouter', () => { const router = await createRouter({ config, logger, + pathPrefix: '/proxy', }); expect(router).toBeDefined(); }); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index b71299ef7e..5c5e17b872 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -17,21 +17,66 @@ import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; -import createProxyMiddleware from 'http-proxy-middleware'; +import createProxyMiddleware, { + Config as ProxyConfig, + Proxy, +} from 'http-proxy-middleware'; import { Logger } from 'winston'; export interface RouterOptions { logger: Logger; config: Config; + // The URL path prefix that the router itself is mounted as, commonly "/proxy" + pathPrefix: string; +} + +// Creates a proxy middleware, possibly with defaults added on top of the +// given config. +function buildMiddleware( + pathPrefix: string, + logger: Logger, + route: string, + config: string | ProxyConfig, +): Proxy { + const fullConfig = + typeof config === 'string' ? { target: config } : { ...config }; + + // Default is to do a path rewrite that strips out the proxy's path prefix + // and the rest of the route. + if (fullConfig.pathRewrite === undefined) { + const routeWithSlash = route.endsWith('/') ? route : `${route}/`; + fullConfig.pathRewrite = { + [`^${pathPrefix}${routeWithSlash}`]: '/', + }; + } + + // Default is to update the Host header to the target + if (fullConfig.changeOrigin === undefined) { + fullConfig.changeOrigin = true; + } + + // Attach the logger to the proxy config + fullConfig.logProvider = () => logger; + + return createProxyMiddleware(fullConfig); } export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); - const proxyConfig = options.config.get('proxy') ?? {}; + + const proxyConfig = options.config.getOptional('proxy') ?? {}; Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { - router.use(route, createProxyMiddleware(proxyRouteConfig)); + router.use( + route, + buildMiddleware( + options.pathPrefix, + options.logger, + route, + proxyRouteConfig, + ), + ); }); return router; diff --git a/plugins/proxy-backend/src/service/standaloneServer.ts b/plugins/proxy-backend/src/service/standaloneServer.ts index f75c044f0f..68e9ade183 100644 --- a/plugins/proxy-backend/src/service/standaloneServer.ts +++ b/plugins/proxy-backend/src/service/standaloneServer.ts @@ -40,6 +40,7 @@ export async function startStandaloneServer( const router = await createRouter({ config, logger, + pathPrefix: '/proxy', }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) diff --git a/plugins/register-component/README.md b/plugins/register-component/README.md index 729a38955c..d324b3941f 100644 --- a/plugins/register-component/README.md +++ b/plugins/register-component/README.md @@ -1,5 +1,92 @@ -# [WIP] register-component +# Register component plugin Welcome to the register-component plugin! This plugin allows you to submit your Backstage component using your software's YAML config. + +When installed it is accessible on [localhost:3000/register-component](localhost:3000/register-component). + + + + + + + +## Standalone setup + +0. Install plugin and its dependency `plugin-catalog` + +```bash +yarn add @backstage/plugin-register-component -W +yarn add @backstage/plugin-catalog -W +``` + +1. Add dependencies to the active plugins list + +```typescript +// packages/app/src/plugins.ts +export { plugin as RegisterComponent } from '@backstage/plugin-register-component'; +export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; +``` + +2. Create `packages/app/src/apis.ts` and register all the needed plugins + +```typescript +import { + alertApiRef, + AlertApiForwarder, + ApiRegistry, + ConfigApi, + errorApiRef, + ErrorApiForwarder, + ErrorAlerter, +} from '@backstage/core'; + +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; + +export const apis = (config: ConfigApi) => { + const backendUrl = config.getString('backend.baseUrl'); + + const builder = ApiRegistry.builder(); + + const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); + const errorApi = builder.add( + errorApiRef, + new ErrorAlerter(alertApi, new ErrorApiForwarder()), + ); + + builder.add( + catalogApiRef, + new CatalogClient({ + apiOrigin: backendUrl, + basePath: '/catalog', + }), + ); + + return builder.build(); +}; +``` + +3. Pass `apis` to createApp + +```typescript +// packages/app/src/App.tsx +import { apis } from './apis'; + +const app = createApp({ + apis, + plugins: Object.values(plugins), +}); +``` + +## Running + +Just run the backstage. + +``` +yarn start && yarn --cwd packages/backend start +``` + +## Usage + +Pretty straightforward, navigate to [localhost:3000/register-component](localhost:3000/register-component) and enter your component's YAML config URL. diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 02fd3fe0ed..0064d50aaa 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,23 +21,23 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.18", - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/plugin-catalog": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/plugin-catalog": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-hook-form": "^5.7.2", + "react-hook-form": "^6.6.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/register-component/src/assets/screenshot-1.png b/plugins/register-component/src/assets/screenshot-1.png new file mode 100644 index 0000000000..ffaebbb2f5 Binary files /dev/null and b/plugins/register-component/src/assets/screenshot-1.png differ diff --git a/plugins/register-component/src/assets/screenshot-2.png b/plugins/register-component/src/assets/screenshot-2.png new file mode 100644 index 0000000000..e4e5dc579c Binary files /dev/null and b/plugins/register-component/src/assets/screenshot-2.png differ diff --git a/plugins/register-component/src/assets/screenshot-3.png b/plugins/register-component/src/assets/screenshot-3.png new file mode 100644 index 0000000000..4bbac74d82 Binary files /dev/null and b/plugins/register-component/src/assets/screenshot-3.png differ diff --git a/plugins/register-component/src/assets/screenshot-4.png b/plugins/register-component/src/assets/screenshot-4.png new file mode 100644 index 0000000000..f45f333135 Binary files /dev/null and b/plugins/register-component/src/assets/screenshot-4.png differ diff --git a/plugins/register-component/src/assets/screenshot-5.png b/plugins/register-component/src/assets/screenshot-5.png new file mode 100644 index 0000000000..a50decd748 Binary files /dev/null and b/plugins/register-component/src/assets/screenshot-5.png differ diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index 01402ca19b..bb7faabc9d 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ +import { cleanup, fireEvent, render } from '@testing-library/react'; import React from 'react'; -import { render, fireEvent, cleanup } from '@testing-library/react'; -import RegisterComponentForm, { Props } from './RegisterComponentForm'; import { act } from 'react-dom/test-utils'; +import RegisterComponentForm, { Props } from './RegisterComponentForm'; const setup = (props?: Partial) => { return { @@ -37,7 +37,7 @@ describe('RegisterComponentForm', () => { const { rendered } = setup(); expect( await rendered.findByText( - 'Enter the full path to the component.yaml file in GitHub to start tracking your component. It must be in a public repo.', + 'Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component.', ), ).toBeInTheDocument(); diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index c1ea9c454d..4090b2af9c 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -14,17 +14,17 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import { BackstageTheme } from '@backstage/theme'; import { Button, FormControl, FormHelperText, - TextField, LinearProgress, + TextField, } from '@material-ui/core'; -import { useForm } from 'react-hook-form'; import { makeStyles } from '@material-ui/core/styles'; -import { BackstageTheme } from '@backstage/theme'; +import React, { FC } from 'react'; +import { useForm } from 'react-hook-form'; import { ComponentIdValidators } from '../../util/validate'; const useStyles = makeStyles(theme => ({ @@ -49,7 +49,7 @@ const RegisterComponentForm: FC = ({ onSubmit, submitting }) => { }); const classes = useStyles(); const hasErrors = !!errors.componentLocation; - const dirty = formState?.dirty; + const dirty = formState?.isDirty; return submitting ? ( @@ -71,7 +71,7 @@ const RegisterComponentForm: FC = ({ onSubmit, submitting }) => { name="componentLocation" required margin="normal" - helperText="Enter the full path to the component.yaml file in GitHub to start tracking your component. It must be in a public repo." + helperText="Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component." inputRef={register({ required: true, validate: ComponentIdValidators, diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 85d63fe2cb..e61f904347 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -79,7 +79,16 @@ const RegisterComponentPage: FC<{}> = () => { setFormState(FormStates.Submitting); const { componentLocation: target } = formData; try { - const data = await catalogApi.addLocation('github', target); + const typeMapping = [ + { url: /https:\/\/gitlab\.com\/.*/, type: 'gitlab' }, + { url: /https:\/\/bitbucket\.org\/.*/, type: 'bitbucket/api' }, + { url: /https:\/\/dev\.azure\.com\/.*/, type: 'azure/api' }, + { url: /.*/, type: 'github' }, + ]; + + const type = typeMapping.filter(item => item.url.test(target))[0].type; + + const data = await catalogApi.addLocation(type, target); if (!isMounted()) return; diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index 68b06d1751..15d0b9ced6 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -63,6 +63,7 @@ export const RegisterComponentResultDialog: FC = ({ .filter(Boolean) .join(':'), kind: entity.kind, + selectedTabId: 'overview', }); return ( diff --git a/plugins/register-component/src/util/validate.test.ts b/plugins/register-component/src/util/validate.test.ts index d062655bd2..2b465a536e 100644 --- a/plugins/register-component/src/util/validate.test.ts +++ b/plugins/register-component/src/util/validate.test.ts @@ -31,11 +31,13 @@ describe('ComponentIdValidators', () => { }); }); describe('yamlValidator', () => { - const errorMessage = "Must end with '.yaml'."; + const errorMessage = "Must contain '.yaml'."; test.each([ [true, '.yaml'], [true, 'http://example.com/blob/master/service.yaml'], [true, 'https://example.yaml'], + [true, 'https://example.com?path=abc.yaml&c=1'], + [errorMessage, 'https://example.com?path=abc_yaml&c=1'], [errorMessage, '.yml'], [errorMessage, 'http://example.com/blob/master/service'], [errorMessage, undefined], diff --git a/plugins/register-component/src/util/validate.ts b/plugins/register-component/src/util/validate.ts index 78d20995f5..9afb5ebd97 100644 --- a/plugins/register-component/src/util/validate.ts +++ b/plugins/register-component/src/util/validate.ts @@ -19,6 +19,6 @@ export const ComponentIdValidators = { (typeof value === 'string' && value.match(/^https:\/\//) !== null) || 'Must start with https://.', yamlValidator: (value: any) => - (typeof value === 'string' && value.match(/.yaml$/) !== null) || - "Must end with '.yaml'.", + (typeof value === 'string' && value.match(/\.yaml/) !== null) || + "Must contain '.yaml'.", }; diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md index aacd0e9b6d..c221dfd23b 100644 --- a/plugins/rollbar-backend/README.md +++ b/plugins/rollbar-backend/README.md @@ -4,9 +4,20 @@ Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API. ## Setup -A `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read access account token. +The following values are read from the configuration file. + +```yaml +rollbar: + organization: spotify + accountToken: + $secret: + env: ROLLBAR_ACCOUNT_TOKEN +``` + +_NOTE: The `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read +access account token._ ## Links -- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/rollbar] -- (The Backstage homepage)[https://backstage.io] +- [Frontend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/rollbar) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 102e233756..96a968ee46 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.21", "@types/express": "^4.17.6", - "axios": "^0.19.2", + "axios": "^0.20.0", "camelcase-keys": "^6.2.2", "compression": "^1.7.4", "cors": "^2.8.5", @@ -36,7 +37,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "@types/supertest": "^2.0.8", "jest-fetch-mock": "^3.0.3", "supertest": "^4.0.2" diff --git a/plugins/rollbar-backend/src/service/router.test.ts b/plugins/rollbar-backend/src/service/router.test.ts index 9ec0dbc7b5..f5dc2f1c3d 100644 --- a/plugins/rollbar-backend/src/service/router.test.ts +++ b/plugins/rollbar-backend/src/service/router.test.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import { RollbarApi } from '../api'; @@ -37,6 +38,9 @@ describe('createRouter', () => { const router = await createRouter({ rollbarApi, logger: getVoidLogger(), + config: ConfigReader.fromConfigs([ + { context: 'abc', data: { rollbar: { accountToken: 'foo' } } }, + ]), }); app = express().use(router); }); diff --git a/plugins/rollbar-backend/src/service/router.ts b/plugins/rollbar-backend/src/service/router.ts index 09491b0d00..5ee63b5b05 100644 --- a/plugins/rollbar-backend/src/service/router.ts +++ b/plugins/rollbar-backend/src/service/router.ts @@ -14,23 +14,29 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; -import { Logger } from 'winston'; -import Router from 'express-promise-router'; import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { errorHandler } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; import { RollbarApi } from '../api'; export interface RouterOptions { rollbarApi?: RollbarApi; logger: Logger; + config: Config; } export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); + const logger = options.logger.child({ plugin: 'rollbar' }); - const accessToken = !options.rollbarApi ? getRollbarAccountToken(logger) : ''; + const config = options.config.getConfig('rollbar'); + const accessToken = !options.rollbarApi + ? getRollbarAccountToken(config, logger) + : ''; if (options.rollbarApi || accessToken) { const rollbarApi = @@ -82,17 +88,20 @@ export async function createRouter( return router; } -function getRollbarAccountToken(logger: Logger) { - const token = process.env.ROLLBAR_ACCOUNT_TOKEN || ''; +function getRollbarAccountToken(config: Config, logger: Logger) { + const token = + config.getOptionalString('accountToken') || + process.env.ROLLBAR_ACCOUNT_TOKEN || + ''; if (!token) { if (process.env.NODE_ENV !== 'development') { throw new Error( - 'Rollbar token must be provided in ROLLBAR_ACCOUNT_TOKEN environment variable to start the API.', + 'The rollbar.accountToken must be provided in config to start the API.', ); } logger.warn( - 'Failed to initialize rollbar backend, set ROLLBAR_ACCOUNT_TOKEN environment variable to start the API.', + 'Failed to initialize rollbar backend, set rollbar.accountToken in config to start the API.', ); } diff --git a/plugins/rollbar-backend/src/service/standaloneServer.ts b/plugins/rollbar-backend/src/service/standaloneServer.ts index ecd5c072fb..a96d30d836 100644 --- a/plugins/rollbar-backend/src/service/standaloneServer.ts +++ b/plugins/rollbar-backend/src/service/standaloneServer.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; +import { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { createRouter } from './router'; export interface ServerOptions { @@ -24,14 +28,16 @@ export interface ServerOptions { enableCors: boolean; logger: Logger; } + export async function startStandaloneServer( options: ServerOptions, ): Promise { const logger = options.logger.child({ service: 'rollbar-backend' }); + const config = ConfigReader.fromConfigs(await loadBackendConfig()); logger.debug('Creating application...'); - const router = await createRouter({ logger }); + const router = await createRouter({ logger, config }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) diff --git a/plugins/rollbar/README.md b/plugins/rollbar/README.md index d0cfae00cc..94ef38b43a 100644 --- a/plugins/rollbar/README.md +++ b/plugins/rollbar/README.md @@ -52,5 +52,5 @@ builder.add( ## Links -- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/rollbar-backend] -- (The Backstage homepage)[https://backstage.io] +- [Backend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/rollbar-backend) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 1ca02cb528..9c5343247c 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,9 +35,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts index 5cabbcbc24..1862ad8270 100644 --- a/plugins/rollbar/src/api/RollbarClient.ts +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -20,20 +20,13 @@ import { RollbarProject, RollbarTopActiveItem, } from './types'; +import { DiscoveryApi } from '@backstage/core'; export class RollbarClient implements RollbarApi { - private apiOrigin: string; - private basePath: string; + private readonly discoveryApi: DiscoveryApi; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; } async getAllProjects(): Promise { @@ -59,7 +52,7 @@ export class RollbarClient implements RollbarApi { } private async get(path: string): Promise { - const url = `${this.apiOrigin}${this.basePath}${path}`; + const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; const response = await fetch(url); if (!response.ok) { diff --git a/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx index 7bda63ea5f..c0524fabc5 100644 --- a/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx +++ b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx @@ -17,6 +17,7 @@ import React, { ReactNode } from 'react'; import { Header, + HeaderLabel, Page, pageTheme, Content, @@ -36,7 +37,10 @@ export const RollbarLayout = ({ title = 'Dashboard', children }: Props) => {
+ > + + +
diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx index 2b0268500d..11263ed820 100644 --- a/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx @@ -15,7 +15,12 @@ */ import * as React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + ConfigApi, + configApiRef, +} from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; @@ -30,11 +35,20 @@ describe('RollbarPage component', () => { const rollbarApi: Partial = { getAllProjects: () => Promise.resolve(projects), }; + const config: Partial = { + getString: () => 'foo', + getOptionalString: () => 'foo', + }; const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( - + {children} , ), diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx index ec07c0fa48..0460b4a308 100644 --- a/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx @@ -16,18 +16,27 @@ import React from 'react'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; +import { configApiRef, useApi } from '@backstage/core'; import { rollbarApiRef } from '../../api/RollbarApi'; import { RollbarLayout } from '../RollbarLayout/RollbarLayout'; import { RollbarProjectTable } from './RollbarProjectTable'; export const RollbarPage = () => { + const configApi = useApi(configApiRef); const rollbarApi = useApi(rollbarApiRef); - const { value, loading } = useAsync(() => rollbarApi.getAllProjects()); + const org = + configApi.getOptionalString('rollbar.organization') ?? + configApi.getString('organization.name'); + const { value, loading, error } = useAsync(() => rollbarApi.getAllProjects()); return ( - + ); }; diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx index bb8299b57e..fdf90c468e 100644 --- a/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx +++ b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx @@ -17,9 +17,14 @@ import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { Link } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import { Table, TableColumn } from '@backstage/core'; import { RollbarProject } from '../../api/types'; +const projectUrl = (org: string, id: number) => + `https://rollbar.com/${org}/all/items/?projects=${id}`; + const columns: TableColumn[] = [ { title: 'ID', @@ -32,7 +37,6 @@ const columns: TableColumn[] = [ title: 'Name', field: 'name', type: 'string', - align: 'left', highlight: true, render: (row: Partial) => ( @@ -44,29 +48,58 @@ const columns: TableColumn[] = [ title: 'Status', field: 'status', type: 'string', - align: 'left', + }, + { + title: 'Open', + width: '10%', + render: (row: any) => ( + + + + ), }, ]; type Props = { projects: RollbarProject[]; loading: boolean; + organization: string; + error?: any; }; -export const RollbarProjectTable = ({ projects, loading }: Props) => { +export const RollbarProjectTable = ({ + projects, + organization, + loading, + error, +}: Props) => { + if (error) { + return ( +
+ + Error encountered while fetching rollbar projects. {error.toString()} + +
+ ); + } + return (
({ organization, ...p }))} /> ); }; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx index 135075bf82..e4919a68bd 100644 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx @@ -15,7 +15,12 @@ */ import * as React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + ConfigApi, + configApiRef, +} from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; @@ -43,11 +48,20 @@ describe('RollbarProjectPage component', () => { const rollbarApi: Partial = { getTopActiveItems: () => Promise.resolve(items), }; + const config: Partial = { + getString: () => 'foo', + getOptionalString: () => 'foo', + }; const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( - + {children} , ), diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx index d5641e58ce..6635a72086 100644 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx @@ -17,17 +17,21 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; +import { configApiRef, useApi } from '@backstage/core'; import { rollbarApiRef } from '../../api/RollbarApi'; import { RollbarLayout } from '../RollbarLayout/RollbarLayout'; import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable'; export const RollbarProjectPage = () => { + const configApi = useApi(configApiRef); const rollbarApi = useApi(rollbarApiRef); + const org = + configApi.getOptionalString('rollbar.organization') ?? + configApi.getString('organization.name'); const { componentId } = useParams() as { componentId: string; }; - const { value, loading } = useAsync(() => + const { value, loading, error } = useAsync(() => rollbarApi .getTopActiveItems(componentId, 168) .then(data => @@ -37,7 +41,13 @@ export const RollbarProjectPage = () => { return ( - + ); }; diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx index 724426ac8f..dc177265cd 100644 --- a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx @@ -41,14 +41,28 @@ const items: RollbarTopActiveItem[] = [ describe('RollbarTopItemsTable component', () => { it('should render empty data message when loaded and no data', async () => { const rendered = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); expect(rendered.getByText(/No records to display/)).toBeInTheDocument(); }); it('should display item attributes when loading has finished', async () => { const rendered = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); expect(rendered.getByText(/1234/)).toBeInTheDocument(); expect(rendered.getByText(/Error in foo/)).toBeInTheDocument(); diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx index 44dd5ed3f0..913ecca28a 100644 --- a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx @@ -16,6 +16,8 @@ import React from 'react'; import { Table, TableColumn } from '@backstage/core'; +import { Link } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import { RollbarFrameworkId, RollbarLevel, @@ -23,6 +25,9 @@ import { } from '../../api/types'; import { RollbarTrendGraph } from '../RollbarTrendGraph/RollbarTrendGraph'; +const itemUrl = (org: string, project: string, id: number) => + `https://rollbar.com/${org}/${project}/items/${id}`; + const columns: TableColumn[] = [ { title: 'ID', @@ -30,6 +35,15 @@ const columns: TableColumn[] = [ type: 'string', align: 'left', width: '70px', + render: (data: any) => ( + + {data.item.counter} + + ), }, { title: 'Title', @@ -40,9 +54,7 @@ const columns: TableColumn[] = [ { title: 'Trend', sorting: false, - render: data => ( - - ), + render: (data: any) => , }, { title: 'Occurrences', @@ -81,22 +93,42 @@ const columns: TableColumn[] = [ type Props = { items: RollbarTopActiveItem[]; + organization: string; + project: string; loading: boolean; + error?: any; }; -export const RollbarTopItemsTable = ({ items, loading }: Props) => { +export const RollbarTopItemsTable = ({ + items, + organization, + project, + loading, + error, +}: Props) => { + if (error) { + return ( +
+ + Error encountered while fetching rollbar top items. {error.toString()} + +
+ ); + } + return (
({ org: organization, project, ...i }))} /> ); }; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index fc956c9832..720eb7cc15 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,12 +21,13 @@ "mock-data": "./scripts/mock-data.sh" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", - "@backstage/catalog-model": "^0.1.1-alpha.18", - "@backstage/config": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.21", "@octokit/rest": "^18.0.0", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", + "command-exists-promise": "^2.0.2", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", @@ -43,11 +44,11 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "@octokit/types": "^5.0.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", - "@types/nodegit": "0.26.7", + "@types/nodegit": "0.26.8", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "yaml": "^1.10.0" diff --git a/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml b/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml index 958882e9fd..a377d60ccc 100644 --- a/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml @@ -5,9 +5,9 @@ metadata: title: Create React App Template description: Create a new CRA website project tags: - - Experimental - - React - - CRA + - experimental + - react + - cra spec: owner: web@example.com templater: cra diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml new file mode 100644 index 0000000000..da2152280d --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml @@ -0,0 +1,29 @@ +apiVersion: backstage.io/v1alpha1 +kind: Template +metadata: + name: docs-template + title: Documentation Template + description: Create a new standalone documentation project + tags: + - experimental + - techdocs + - mkdocs +spec: + owner: spotify/techdocs-core + templater: cookiecutter + type: documentation + path: '.' + + schema: + required: + - component_id + properties: + component_id: + title: Name + type: string + description: Unique name of the component + description: + title: Description + type: string + description: Description of the component + diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml new file mode 100644 index 0000000000..854b692fcf --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: {{cookiecutter.component_id}} + description: {{cookiecutter.description}} + annotations: + github.com/project-slug: {{cookiecutter.storePath}} + backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}} +spec: + type: documentation + lifecycle: experimental + owner: {{cookiecutter.owner}} diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/docs/index.md b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/docs/index.md new file mode 100644 index 0000000000..5352ef7801 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/docs/index.md @@ -0,0 +1,28 @@ +## {{ cookiecutter.component_id }} + +{{ cookiecutter.description }} + +## Getting started + +Start write your documentation by adding more markdown (.md) files to this folder (/docs) or replace the content in this file. + +## Table of Contents + +The Table of Contents on the right is generated automatically based on the hierarchy +of headings. Only use one H1 (`#` in Markdown) per file. + +## Site navigation + +For new pages to appear in the left hand navigation you need edit the `mkdocs.yml` +file in root of your repo. The navigation can also link out to other sites. + +Alternatively, if there is no `nav` section in `mkdocs.yml`, a navigation section +will be created for you. However, you will not be able to use alternate titles for +pages, or include links to other sites. + +Note that MkDocs uses `mkdocs.yml`, not `mkdocs.yaml`, although both appear to work. +See also . + +## Support + +That's it. If you need support, reach out in [#docs-like-code](https://discord.com/channels/687207715902193673/714754240933003266) on Discord. diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml new file mode 100644 index 0000000000..2126971502 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml @@ -0,0 +1,8 @@ +site_name: {{cookiecutter.component_id}} +site_description: {{cookiecutter.description}} + +nav: + - Introduction: index.md + +plugins: + - techdocs-core diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml index 5594a3a919..ee051de6e0 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -5,8 +5,8 @@ metadata: title: React SSR Template description: Create a website powered with Next.js tags: - - Recommended - - React + - recommended + - react spec: owner: web@example.com templater: cookiecutter diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml index 65a77d4a27..832b539ac6 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml @@ -6,6 +6,7 @@ metadata: annotations: github.com/project-slug: {{cookiecutter.storePath}} backstage.io/github-actions-id: {{cookiecutter.storePath}} + backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}} spec: type: website lifecycle: experimental diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/docs/index.md b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/docs/index.md new file mode 100644 index 0000000000..2a0f572ff2 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/docs/index.md @@ -0,0 +1,28 @@ +# {{ cookiecutter.component_id }} + +{{ cookiecutter.description }} + +## Getting started + +Start writing your documentation by adding more markdown (.md) files to this folder (/docs) or replace the content in this file. + +## Table of Contents + +The Table of Contents on the right is generated automatically based on the hierarchy +of headings. Only use one H1 (`#` in Markdown) per file. + +## Site navigation + +For new pages to appear in the left hand navigation you need edit the `mkdocs.yml` +file in root of your repo. The navigation can also link out to other sites. + +Alternatively, if there is no `nav` section in `mkdocs.yml`, a navigation section +will be created for you. However, you will not be able to use alternate titles for +pages, or include links to other sites. + +Note that MkDocs uses `mkdocs.yml`, not `mkdocs.yaml`, although both appear to work. +See also . + +## Support + +That's it. If you need support, reach out in [#docs-like-code](https://discord.com/channels/687207715902193673/714754240933003266) on Discord. diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml new file mode 100644 index 0000000000..0d10d11063 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml @@ -0,0 +1,8 @@ +site_name: {{cookiecutter.component_id}} +site_description: {{cookiecutter.description}} + +nav: + - Introduction: index.md + +plugins: + - techdocs-core diff --git a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml index 520d0a62ac..c6189fe7c8 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml @@ -5,8 +5,8 @@ metadata: title: Spring Boot GRPC Service description: Create a simple microservice using gRPC and Spring Boot Java tags: - - Recommended - - Java + - recommended + - java spec: owner: service@example.com templater: cookiecutter diff --git a/plugins/scaffolder-backend/scripts/mock-data.sh b/plugins/scaffolder-backend/scripts/mock-data.sh index f18d4b4838..9a9b0b186b 100755 --- a/plugins/scaffolder-backend/scripts/mock-data.sh +++ b/plugins/scaffolder-backend/scripts/mock-data.sh @@ -4,6 +4,7 @@ for URL in \ 'react-ssr-template' \ 'springboot-grpc-template' \ 'create-react-app' \ + 'docs-template' \ ; do \ curl \ --location \ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 164ec316df..93493ab2cc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -45,7 +45,7 @@ const { mockRemote: jest.Mocked; }; -describe('Github Publisher', () => { +describe('GitHub Publisher', () => { const publisher = new GithubPublisher({ client: new Octokit() }); beforeEach(() => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index fff349cf82..21b6fc3dde 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -15,11 +15,13 @@ */ import fs from 'fs-extra'; import { JsonValue } from '@backstage/config'; -import { runDockerContainer } from './helpers'; +import { runDockerContainer, runCommand } from './helpers'; import { TemplaterBase, TemplaterRunOptions } from '.'; import path from 'path'; import { TemplaterRunResult } from './types'; +const commandExists = require('command-exists-promise'); + export class CookieCutter implements TemplaterBase { private async fetchTemplateCookieCutter( directory: string, @@ -51,21 +53,30 @@ export class CookieCutter implements TemplaterBase { const templateDir = options.directory; const resultDir = await fs.promises.mkdtemp(`${options.directory}-result`); - await runDockerContainer({ - imageName: 'spotify/backstage-cookiecutter', - args: [ - 'cookiecutter', - '--no-input', - '-o', - '/result', - '/template', - '--verbose', - ], - templateDir, - resultDir, - logStream: options.logStream, - dockerClient: options.dockerClient, - }); + const cookieCutterInstalled = await commandExists('cookiecutter'); + if (cookieCutterInstalled) { + await runCommand({ + command: 'cookiecutter', + args: ['--no-input', '-o', resultDir, templateDir, '--verbose'], + logStream: options.logStream, + }); + } else { + await runDockerContainer({ + imageName: 'spotify/backstage-cookiecutter', + args: [ + 'cookiecutter', + '--no-input', + '-o', + '/result', + '/template', + '--verbose', + ], + templateDir, + resultDir, + logStream: options.logStream, + dockerClient: options.dockerClient, + }); + } return { resultDir: path.resolve(resultDir, options.values.component_id as string), diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts index a3dea7ea5a..e71b63bfb0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts @@ -18,6 +18,7 @@ import Docker from 'dockerode'; import fs from 'fs'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; +import { spawn } from 'child_process'; export type RunDockerContainerOptions = { imageName: string; @@ -29,6 +30,12 @@ export type RunDockerContainerOptions = { createOptions?: Docker.ContainerCreateOptions; }; +export type RunCommandOptions = { + command: string; + args: string[]; + logStream?: Writable; +}; + /** * Gets the templater key to use for templating from the entity * @param entity Template entity @@ -43,6 +50,42 @@ export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => { return templater; }; +/** + * + * @param options the options object + * @param options.command the command to run + * @param options.args the arguments to pass the command + * @param options.logStream the log streamer to capture log messages + */ +export const runCommand = async ({ + command, + args, + logStream = new PassThrough(), +}: RunCommandOptions) => { + await new Promise((resolve, reject) => { + const process = spawn(command, args); + + process.stdout.on('data', stream => { + logStream.write(stream); + }); + + process.stderr.on('data', stream => { + logStream.write(stream); + }); + + process.on('error', error => { + return reject(error); + }); + + process.on('close', code => { + if (code !== 0) { + return reject(`Command ${command} failed, exit code: ${code}`); + } + return resolve(); + }); + }); +}; + /** * * @param options the options object diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts index 6a0fe60858..5b01960612 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts @@ -36,7 +36,7 @@ export type TemplaterRunResult = { }; /** - * The values that the templater will recieve. The directory of the + * The values that the templater will receive. The directory of the * skeleton, with the values from the frontend. A dedicated log stream and a docker * client to run any templater on top of your directory. */ diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index fcfd5765a1..1b96158fa4 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -42,6 +42,7 @@ export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); + router.use(express.json()); const { preparers, diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index 5893ee86ba..a356cb8ec9 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -6,5 +6,5 @@ This is the frontend part of the default scaffolder plugin. ## Links -- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/scaffolder-backend] -- (The Backstage homepage)[https://backstage.io] +- [Backend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/scaffolder-backend) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 822a620c8a..7497cbe024 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.18", - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/plugin-catalog": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/plugin-catalog": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -38,12 +38,12 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "swr": "^0.2.2" + "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 4a0d508a2b..3c42ed2ca4 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core'; +import { createApiRef, DiscoveryApi } from '@backstage/core'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; export const scaffolderApiRef = createApiRef({ @@ -23,18 +23,10 @@ export const scaffolderApiRef = createApiRef({ }); export class ScaffolderApi { - private apiOrigin: string; - private basePath: string; + private readonly discoveryApi: DiscoveryApi; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; } /** @@ -46,7 +38,7 @@ export class ScaffolderApi { template: TemplateEntityV1alpha1, values: Record, ) { - const url = `${this.apiOrigin}${this.basePath}/jobs`; + const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`; const response = await fetch(url, { method: 'POST', headers: { @@ -65,9 +57,9 @@ export class ScaffolderApi { } async getJob(jobId: string) { - const url = `${this.apiOrigin}${this.basePath}/job/${encodeURIComponent( - jobId, - )}`; + const url = `${await this.discoveryApi.getBaseUrl( + 'scaffolder', + )}/v1/job/${encodeURIComponent(jobId)}`; return fetch(url).then(x => x.json()); } } diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 12b3e303dd..6f8bf0d550 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -64,7 +64,7 @@ export const ScaffolderPage: React.FC<{}> = () => { }, [error, errorApi]); return ( - +
= () => { Shoot! Looks like you don't have any templates. Check out the documentation{' '} - + here! diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 7d38c6f223..d98b1a11e9 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -56,7 +56,7 @@ export const TemplateCard = ({ name, }: TemplateCardProps) => { const theme = pageTheme[type] ?? pageTheme.other; - const [gradientStart, gradientStop] = theme.gradient.colors; + const [gradientStart, gradientStop] = theme.colors; const classes = useStyles({ gradientStart, gradientStop }); const href = generatePath(templateRoute.path, { templateName: name }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 05cc249e41..3c22e8d50c 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -136,7 +136,7 @@ export const TemplatePage = () => { } return ( - +
{ const router = Router(); + router.use(express.json()); + const SENTRY_TOKEN = process.env.SENTRY_TOKEN; if (!SENTRY_TOKEN) { if (process.env.NODE_ENV !== 'development') { diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md index cce2228094..73cca29d34 100644 --- a/plugins/sentry/README.md +++ b/plugins/sentry/README.md @@ -10,7 +10,7 @@ Your plugin has been added to the example app in this repository, meaning you'll You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. Needs SENTRY_TOKEN set in the environment for the backend to startup diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 3a6cf54199..d5b29cc307 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,8 +34,8 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index b2a7be7341..4039f40878 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/test-utils-core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/test-utils-core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,8 +35,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/techdocs-backend/.eslintrc.js b/plugins/techdocs-backend/.eslintrc.js index 16a033dbc6..595853b48b 100644 --- a/plugins/techdocs-backend/.eslintrc.js +++ b/plugins/techdocs-backend/.eslintrc.js @@ -1,3 +1,4 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], + ignorePatterns: ['static/**'], }; diff --git a/plugins/techdocs-backend/README.md b/plugins/techdocs-backend/README.md index ad98d07f95..6a54971788 100644 --- a/plugins/techdocs-backend/README.md +++ b/plugins/techdocs-backend/README.md @@ -6,17 +6,45 @@ This is the backend part of the techdocs plugin. This backend plugin can be started in a standalone mode from directly in this package with `yarn start`. However, it will have limited functionality and that process is -most convenient when developing the catalog backend plugin itself. +most convenient when developing the techdocs backend plugin itself. -To evaluate the catalog and have a greater amount of functionality available, instead do +To evaluate TechDocs and have a greater amount of functionality available, instead do ```bash # in one terminal window, run this from from the very root of the Backstage project cd packages/backend yarn start + +# open another terminal window, and run the following from the very root of the Backstage project +yarn lerna run mock-data ``` +## What techdocs-backend does + +This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/techdocs/static/docs`. + +```yaml +techdocs: + storageUrl: http://localhost:7000/techdocs/static/docs +``` + +## Extending techdocs-backend + +Currently the build process of techdocs-backend is split up in these three stages. + +- Preparers +- Generators +- Publishers + +Preparers read your entity data and creates a working directory with your documentation source code. For example if you have set your `backstage.io/techdocs-ref` to `github:https://github.com/spotify/backstage.git` it will clone that repository to a temp folder and pass that on to the generator. + +Generators takes the prepared source and runs the `techdocs-container` on it. It then passes on the output folder of that build to the publisher. + +Publishers gets a folder path from the generator and publish it to your storage solution. Currently the only built in storage solution is a folder called `static/docs` inside the techdocs-backend plugin. + +Any of these can be extended. If we want to publish to a external static file server using rsync for example that can be done by creating a rsync publisher. _(Keep in mind that if you want techdocs-backend to initiate a build this would also require techdocs-backend to act as a proxy, which is not yet implemented.)_ + ## Links -- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/techdocs] -- (The Backstage homepage)[https://backstage.io] +- [Frontend part of the plugin](https://github.com/spotify/backstage/tree/master/plugins/techdocs) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md b/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md new file mode 100644 index 0000000000..bbd98558e0 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/sub-page.md @@ -0,0 +1 @@ +# Another page in our documentation diff --git a/plugins/techdocs-backend/examples/documented-component/documented-component.yaml b/plugins/techdocs-backend/examples/documented-component/documented-component.yaml index d5a1214b15..045ff89013 100644 --- a/plugins/techdocs-backend/examples/documented-component/documented-component.yaml +++ b/plugins/techdocs-backend/examples/documented-component/documented-component.yaml @@ -4,7 +4,7 @@ metadata: name: documented-component description: A Service with TechDocs documentation annotations: - backstage.io/techdocs-ref: 'dir:./' + backstage.io/techdocs-ref: 'github:https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component' spec: type: service lifecycle: experimental diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index 6a3f85d020..3b48e8edff 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -2,6 +2,7 @@ site_name: 'example-docs' nav: - Home: index.md + - SubPage: sub-page.md plugins: - techdocs-core diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 8a5a9fd10a..00650ccca8 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,21 +21,23 @@ "mock-data": "./scripts/mock-data.sh" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.18", - "@backstage/catalog-model": "^0.1.1-alpha.18", - "@backstage/config": "^0.1.1-alpha.18", + "@backstage/backend-common": "^0.1.1-alpha.21", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/config": "^0.1.1-alpha.21", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", "dockerode": "^3.2.1", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.1", + "git-url-parse": "^11.1.3", "knex": "^0.21.1", "node-fetch": "^2.6.0", + "nodegit": "^0.27.0", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", "@types/node-fetch": "^2.5.7", "supertest": "^4.0.2" }, diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index b28010e059..c266f98677 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -24,12 +24,13 @@ import { createRouter } from './router'; describe('createRouter', () => { let app: express.Express; + const logger = getVoidLogger(); beforeAll(async () => { const router = await createRouter({ preparers: new Preparers(), generators: new Generators(), - publisher: new LocalPublish(), + publisher: new LocalPublish(logger), logger: getVoidLogger(), dockerClient: new Docker(), config: ConfigReader.fromConfigs([]), diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 874ab7c89c..bb26ea19b8 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -19,7 +19,6 @@ import express from 'express'; import Knex from 'knex'; import fetch from 'node-fetch'; import { Config } from '@backstage/config'; -import path from 'path'; import Docker from 'dockerode'; import { GeneratorBuilder, @@ -27,6 +26,7 @@ import { PublisherBase, LocalPublish, } from '../techdocs'; +import { resolvePackagePath } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; type RouterOptions = { @@ -39,15 +39,51 @@ type RouterOptions = { dockerClient: Docker; }; +const staticDocsDir = resolvePackagePath( + '@backstage/plugin-techdocs-backend', + 'static/docs', +); + export async function createRouter({ preparers, generators, publisher, config, dockerClient, + logger, }: RouterOptions): Promise { const router = Router(); + const getEntityId = (entity: Entity) => { + return `${entity.kind}:${entity.metadata.namespace ?? ''}:${ + entity.metadata.name + }`; + }; + + const buildDocsForEntity = async (entity: Entity) => { + const preparer = preparers.get(entity); + const generator = generators.get(entity); + + logger.info(`[TechDocs] Running preparer on entity ${getEntityId(entity)}`); + const preparedDir = await preparer.prepare(entity); + + logger.info( + `[TechDocs] Running generator on entity ${getEntityId(entity)}`, + ); + const { resultDir } = await generator.run({ + directory: preparedDir, + dockerClient, + }); + + logger.info( + `[TechDocs] Running publisher on entity ${getEntityId(entity)}`, + ); + await publisher.publish({ + entity, + directory: resultDir, + }); + }; + router.get('/', async (_, res) => { res.status(200).send('Hello TechDocs Backend'); }); @@ -64,27 +100,30 @@ export async function createRouter({ ); entitiesWithDocs.forEach(async entity => { - const preparer = preparers.get(entity); - const generator = generators.get(entity); - - const { resultDir } = await generator.run({ - directory: await preparer.prepare(entity), - dockerClient, - }); - - publisher.publish({ - entity, - directory: resultDir, - }); + await buildDocsForEntity(entity); }); res.send('Successfully generated documentation'); }); if (publisher instanceof LocalPublish) { + router.use('/static/docs/', express.static(staticDocsDir)); router.use( - '/static/docs/', - express.static(path.resolve(__dirname, `../../static/docs`)), + '/static/docs/:kind/:namespace/:name', + async (req, res, next) => { + const baseUrl = config.getString('backend.baseUrl'); + const { kind, namespace, name } = req.params; + + const entityResponse = await fetch( + `${baseUrl}/catalog/entities/by-name/${kind}/${namespace}/${name}`, + ); + if (!entityResponse.ok) next(); + const entity = (await entityResponse.json()) as Entity; + + await buildDocsForEntity(entity); + + res.redirect(req.originalUrl); + }, ); } diff --git a/plugins/techdocs-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts index 76de973875..93233af4c0 100644 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ b/plugins/techdocs-backend/src/service/standaloneServer.ts @@ -41,14 +41,14 @@ export async function startStandaloneServer( logger.debug('Creating application...'); const preparers = new Preparers(); - const directoryPreparer = new DirectoryPreparer(); + const directoryPreparer = new DirectoryPreparer(logger); preparers.register('dir', directoryPreparer); const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(); + const techdocsGenerator = new TechdocsGenerator(logger); generators.register('techdocs', techdocsGenerator); - const publisher = new LocalPublish(); + const publisher = new LocalPublish(logger); const dockerClient = new Docker(); diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/generators.test.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/generators.test.ts new file mode 100644 index 0000000000..b339aef9f8 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/generators.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Generators, TechdocsGenerator } from './'; +import { getVoidLogger } from '@backstage/backend-common'; + +const logger = getVoidLogger(); + +const mockEntity = { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'testName', + }, +}; + +describe('generators', () => { + it('should return error if no generator is registered', async () => { + const generators = new Generators(); + + expect(() => generators.get(mockEntity)).toThrowError( + 'No generator registered for entity: "techdocs"', + ); + }); + + it('should return correct registered generator', async () => { + const generators = new Generators(); + const techdocs = new TechdocsGenerator(logger); + + generators.register('techdocs', techdocs); + + expect(generators.get(mockEntity)).toBe(techdocs); + }); +}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts index da9d3c418b..f28a169942 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts @@ -15,32 +15,29 @@ */ import { - GeneratorBase, - SupportedGeneratorKey, - GeneratorBuilder, - } from './types'; - - import { Entity } from '@backstage/catalog-model'; - import { getGeneratorKey } from './helpers'; - - export class Generators implements GeneratorBuilder { - private generatorMap = new Map(); - - register(templaterKey: SupportedGeneratorKey, templater: GeneratorBase) { - this.generatorMap.set(templaterKey, templater); - } - - get(entity: Entity): GeneratorBase { - const generatorKey = getGeneratorKey(entity); - const generator = this.generatorMap.get(generatorKey); - - if (!generator) { - throw new Error( - `No generator registered for entity: "${generatorKey}"`, - ); - } - - return generator; - } + GeneratorBase, + SupportedGeneratorKey, + GeneratorBuilder, +} from './types'; + +import { Entity } from '@backstage/catalog-model'; +import { getGeneratorKey } from './helpers'; + +export class Generators implements GeneratorBuilder { + private generatorMap = new Map(); + + register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase) { + this.generatorMap.set(generatorKey, generator); } - \ No newline at end of file + + get(entity: Entity): GeneratorBase { + const generatorKey = getGeneratorKey(entity); + const generator = this.generatorMap.get(generatorKey); + + if (!generator) { + throw new Error(`No generator registered for entity: "${generatorKey}"`); + } + + return generator; + } +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts new file mode 100644 index 0000000000..799e79ef67 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Stream, { PassThrough } from 'stream'; +import os from 'os'; +import Docker from 'dockerode'; +import { runDockerContainer, getGeneratorKey } from './helpers'; + +const mockEntity = { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'testName', + }, +}; + +const mockDocker = new Docker() as jest.Mocked; + +describe('helpers', () => { + describe('getGeneratorKey', () => { + it('should return techdocs as the only generator key', () => { + const key = getGeneratorKey(mockEntity); + expect(key).toBe('techdocs'); + }); + }); + + describe('runDockerContainer', () => { + beforeEach(() => { + jest.spyOn(mockDocker, 'pull').mockImplementation((async ( + _image: string, + _something: any, + handler: (err: Error | undefined, stream: PassThrough) => void, + ) => { + const mockStream = new PassThrough(); + handler(undefined, mockStream); + mockStream.end(); + }) as any); + + jest + .spyOn(mockDocker, 'run') + .mockResolvedValue([{ Error: null, StatusCode: 0 }]); + }); + + const imageName = 'spotify/techdocs'; + const args = ['build', '-d', '/result']; + const docsDir = os.tmpdir(); + const resultDir = os.tmpdir(); + + it('should pull the techdocs docker container', async () => { + await runDockerContainer({ + imageName, + args, + docsDir, + resultDir, + dockerClient: mockDocker, + }); + + expect(mockDocker.pull).toHaveBeenCalledWith( + imageName, + {}, + expect.any(Function), + ); + }); + + it('should run the techdocs docker container', async () => { + await runDockerContainer({ + imageName, + args, + docsDir, + resultDir, + dockerClient: mockDocker, + }); + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + expect.any(Stream), + { + Volumes: { + '/content': {}, + '/result': {}, + }, + WorkingDir: '/content', + HostConfig: { + Binds: [`${docsDir}:/content`, `${resultDir}:/result`], + }, + }, + ); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts index 7dc4403977..88667f9903 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts @@ -47,6 +47,16 @@ export async function runDockerContainer({ dockerClient, createOptions, }: RunDockerContainerOptions) { + await new Promise((resolve, reject) => { + dockerClient.pull(imageName, {}, (err, stream) => { + if (err) return reject(err); + stream.pipe(logStream, { end: false }); + stream.on('end', () => resolve()); + stream.on('error', (error: Error) => reject(error)); + return undefined; + }); + }); + const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run( imageName, args, diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts index d6650fb744..40cd7590b6 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts @@ -13,36 +13,59 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import fs from 'fs-extra'; +import path from 'path'; +import os from 'os'; +import { Logger } from 'winston'; + import { GeneratorBase, GeneratorRunOptions, GeneratorRunResult, } from './types'; import { runDockerContainer } from './helpers'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; export class TechdocsGenerator implements GeneratorBase { + private readonly logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } + public async run({ directory, logStream, dockerClient, }: GeneratorRunOptions): Promise { - const resultDir = fs.mkdtempSync(path.join(os.tmpdir(), `techdocs-tmp-`)); - - await runDockerContainer({ - imageName: 'spotify/techdocs', - args: ['build', '-d', '/result'], - logStream, - docsDir: directory, - resultDir, - dockerClient, - }); - - console.log( - `[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`, + const tmpdirPath = os.tmpdir(); + // Fixes a problem with macOS returning a path that is a symlink + const tmpdirResolvedPath = fs.realpathSync(tmpdirPath); + const resultDir = fs.mkdtempSync( + path.join(tmpdirResolvedPath, 'techdocs-tmp-'), ); + + try { + await runDockerContainer({ + imageName: 'spotify/techdocs', + args: ['build', '-d', '/result'], + logStream, + docsDir: directory, + resultDir, + dockerClient, + }); + this.logger.info( + `[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`, + ); + } catch (error) { + this.logger.debug( + `[TechDocs]: Failed to generate docs from ${directory} into ${resultDir}`, + ); + throw new Error( + `Failed to generate docs from ${directory} into ${resultDir} with error ${error.message}`, + ); + } + return { resultDir }; } } diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts index 4292aa68f6..398e4dcdd0 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts @@ -26,7 +26,7 @@ export type GeneratorRunResult = { }; /** - * The values that the generator will recieve. The directory of the + * The values that the generator will receive. The directory of the * uncompiled documentation, with the values from the frontend. A dedicated log stream and a docker * client to run any generator on top of your directory. */ diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts index 8a99a8b59a..1635995b9e 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts @@ -14,6 +14,15 @@ * limitations under the License. */ import { DirectoryPreparer } from './dir'; +import { getVoidLogger } from '@backstage/backend-common'; +import { checkoutGitRepository } from './helpers'; + +jest.mock('./helpers', () => ({ + ...jest.requireActual<{}>('./helpers'), + checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'), +})); + +const logger = getVoidLogger(); const createMockEntity = (annotations: {}) => { return { @@ -30,7 +39,7 @@ const createMockEntity = (annotations: {}) => { describe('directory preparer', () => { it('should merge managed-by-location and techdocs-ref when techdocs-ref is relative', async () => { - const directoryPreparer = new DirectoryPreparer(); + const directoryPreparer = new DirectoryPreparer(logger); const mockEntity = createMockEntity({ 'backstage.io/managed-by-location': @@ -44,7 +53,7 @@ describe('directory preparer', () => { }); it('should merge managed-by-location and techdocs-ref when techdocs-ref is absolute', async () => { - const directoryPreparer = new DirectoryPreparer(); + const directoryPreparer = new DirectoryPreparer(logger); const mockEntity = createMockEntity({ 'backstage.io/managed-by-location': @@ -56,4 +65,19 @@ describe('directory preparer', () => { '/our-documentation/techdocs', ); }); + + it('should merge managed-by-location and techdocs-ref when managed-by-location is a git repository', async () => { + const directoryPreparer = new DirectoryPreparer(logger); + + const mockEntity = createMockEntity({ + 'backstage.io/managed-by-location': + 'github:https://github.com/spotify/backstage/blob/master/catalog-info.yaml', + 'backstage.io/techdocs-ref': 'dir:./docs', + }); + + expect(await directoryPreparer.prepare(mockEntity)).toEqual( + '/tmp/backstage-repo/org/name/branch/docs', + ); + expect(checkoutGitRepository).toHaveBeenCalledTimes(1); + }); }); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts index e61a3e48ac..cc0416c331 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts @@ -16,23 +16,55 @@ import { PreparerBase } from './types'; import { Entity } from '@backstage/catalog-model'; import path from 'path'; -import { parseReferenceAnnotation } from './helpers'; +import { parseReferenceAnnotation, checkoutGitRepository } from './helpers'; +import { InputError } from '@backstage/backend-common'; +import parseGitUrl from 'git-url-parse'; +import { Logger } from 'winston'; export class DirectoryPreparer implements PreparerBase { - prepare(entity: Entity): Promise { - const { location: managedByLocation } = parseReferenceAnnotation( + private readonly logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } + + private async resolveManagedByLocationToDir(entity: Entity) { + const { type, target } = parseReferenceAnnotation( 'backstage.io/managed-by-location', entity, ); - const { location: techdocsLocation } = parseReferenceAnnotation( + + this.logger.debug( + `[TechDocs] Building docs for entity with type 'dir' and managed-by-location '${type}'`, + ); + switch (type) { + case 'github': { + const parsedGitLocation = parseGitUrl(target); + const repoLocation = await checkoutGitRepository(target); + + return path.dirname( + path.join(repoLocation, parsedGitLocation.filepath), + ); + } + case 'file': + return path.dirname(target); + default: + throw new InputError(`Unable to resolve location type ${type}`); + } + } + + async prepare(entity: Entity): Promise { + const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); - const managedByLocationDirectory = path.dirname(managedByLocation); + const managedByLocationDirectory = await this.resolveManagedByLocationToDir( + entity, + ); return new Promise(resolve => { - resolve(path.resolve(managedByLocationDirectory, techdocsLocation)); + resolve(path.resolve(managedByLocationDirectory, target)); }); } } diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts new file mode 100644 index 0000000000..99a7d3bb5c --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { GithubPreparer } from './github'; +import { checkoutGithubRepository } from './helpers'; + +jest.mock('./helpers', () => ({ + ...jest.requireActual<{}>('./helpers'), + checkoutGithubRepository: jest.fn( + () => '/tmp/backstage-repo/org/name/branch', + ), +})); + +const createMockEntity = (annotations = {}) => { + return { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'test-component-name', + annotations: { + ...annotations, + }, + }, + }; +}; + +const logger = getVoidLogger(); + +describe('github preparer', () => { + it('should prepare temp docs path from github repo', async () => { + const preparer = new GithubPreparer(logger); + + const mockEntity = createMockEntity({ + 'backstage.io/techdocs-ref': + 'github:https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', + }); + + const tempDocsPath = await preparer.prepare(mockEntity); + expect(checkoutGithubRepository).toHaveBeenCalledTimes(1); + expect(tempDocsPath).toEqual( + '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', + ); + }); +}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts new file mode 100644 index 0000000000..242b19c24d --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import path from 'path'; +import { Entity } from '@backstage/catalog-model'; +import { InputError } from '@backstage/backend-common'; +import { PreparerBase } from './types'; +import parseGitUrl from 'git-url-parse'; +import { parseReferenceAnnotation, checkoutGithubRepository } from './helpers'; +import { Logger } from 'winston'; + +export class GithubPreparer implements PreparerBase { + private readonly logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } + + async prepare(entity: Entity): Promise { + const { type, target } = parseReferenceAnnotation( + 'backstage.io/techdocs-ref', + entity, + ); + + if (type !== 'github') { + throw new InputError(`Wrong target type: ${type}, should be 'github'`); + } + + try { + const repoPath = await checkoutGithubRepository(target); + + const parsedGitLocation = parseGitUrl(target); + return path.join(repoPath, parsedGitLocation.filepath); + } catch (error) { + this.logger.debug(`Repo checkout failed with error ${error.message}`); + throw error; + } + } +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts index 1fd86805ca..d962242262 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts @@ -16,10 +16,15 @@ import { Entity } from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; import { RemoteProtocol } from './types'; +import parseGitUrl from 'git-url-parse'; +import { Clone, Repository } from 'nodegit'; +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; export type ParsedLocationAnnotation = { - protocol: RemoteProtocol; - location: string; + type: RemoteProtocol; + target: string; }; export const parseReferenceAnnotation = ( @@ -36,19 +41,98 @@ export const parseReferenceAnnotation = ( // split on the first colon for the protocol and the rest after the first split // is the location. - const [protocol, location] = annotation.split(/:(.+)/) as [ + const [type, target] = annotation.split(/:(.+)/) as [ RemoteProtocol?, string?, ]; - if (!protocol || !location) { + if (!type || !target) { throw new InputError( `Failure to parse either protocol or location for entity: ${entity.metadata.name}`, ); } return { - protocol, - location, + type, + target, }; }; + +export const clearGithubRepositoryCache = () => { + fs.removeSync(path.join(fs.realpathSync(os.tmpdir()), 'backstage-repo')); +}; + +export const checkoutGitRepository = async ( + repoUrl: string, +): Promise => { + const parsedGitLocation = parseGitUrl(repoUrl); + + const repositoryTmpPath = path.join( + // fs.realpathSync fixes a problem with macOS returning a path that is a symlink + fs.realpathSync(os.tmpdir()), + 'backstage-repo', + parsedGitLocation.source, + parsedGitLocation.owner, + parsedGitLocation.name, + parsedGitLocation.ref, + ); + + if (fs.existsSync(repositoryTmpPath)) { + const repository = await Repository.open(repositoryTmpPath); + const currentBranchName = (await repository.getCurrentBranch()).shorthand(); + await repository.mergeBranches( + currentBranchName, + `origin/${currentBranchName}`, + ); + return repositoryTmpPath; + } + + const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + + fs.mkdirSync(repositoryTmpPath, { recursive: true }); + await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath, {}); + + return repositoryTmpPath; +}; + +// Could be merged with checkoutGitRepository +export const checkoutGithubRepository = async ( + repoUrl: string, +): Promise => { + const parsedGitLocation = parseGitUrl(repoUrl); + + // Should propably not be hardcoded names of env variables, but seems too hard to access config down here + const user = process.env.GITHUB_PRIVATE_TOKEN_USER || ''; + const token = process.env.GITHUB_PRIVATE_TOKEN || ''; + + const repositoryTmpPath = path.join( + // fs.realpathSync fixes a problem with macOS returning a path that is a symlink + fs.realpathSync(os.tmpdir()), + 'backstage-repo', + parsedGitLocation.source, + parsedGitLocation.owner, + parsedGitLocation.name, + parsedGitLocation.ref, + ); + + if (fs.existsSync(repositoryTmpPath)) { + const repository = await Repository.open(repositoryTmpPath); + const currentBranchName = (await repository.getCurrentBranch()).shorthand(); + await repository.mergeBranches( + currentBranchName, + `origin/${currentBranchName}`, + ); + return repositoryTmpPath; + } + + if (user && token) { + parsedGitLocation.token = `${user}:${token}`; + } + + const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + + fs.mkdirSync(repositoryTmpPath, { recursive: true }); + await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath); + + return repositoryTmpPath; +}; diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts index 9f928e7413..535b56f327 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ export { DirectoryPreparer } from './dir'; +export { GithubPreparer } from './github'; export { Preparers } from './preparers'; export type { PreparerBuilder } from './types'; diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts index 1d5b689d8b..1c8fadd145 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts @@ -26,13 +26,16 @@ export class Preparers implements PreparerBuilder { } get(entity: Entity): PreparerBase { - const { protocol } = parseReferenceAnnotation('backstage.io/techdocs-ref', entity); - const preparer = this.preparerMap.get(protocol); + const { type } = parseReferenceAnnotation( + 'backstage.io/techdocs-ref', + entity, + ); + const preparer = this.preparerMap.get(type); if (!preparer) { - throw new Error(`No preparer registered for type: "${protocol}"`); + throw new Error(`No preparer registered for type: "${type}"`); } return preparer; } -} \ No newline at end of file +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts index b5a96e4feb..55e0a547e9 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LocalPublish } from './local'; import fs from 'fs-extra'; import path from 'path'; +import { getVoidLogger } from '@backstage/backend-common'; +import { LocalPublish } from './local'; const createMockEntity = (annotations = {}) => { return { @@ -30,9 +31,11 @@ const createMockEntity = (annotations = {}) => { }; }; +const logger = getVoidLogger(); + describe('local publisher', () => { it('should publish generated documentation dir', async () => { - const publisher = new LocalPublish(); + const publisher = new LocalPublish(logger); const mockEntity = createMockEntity(); @@ -48,8 +51,13 @@ describe('local publisher', () => { `../../../../static/docs/${mockEntity.metadata.name}`, ); - expect(fs.existsSync(publishDir)).toBeTruthy(); - expect(fs.existsSync(path.join(publishDir, '/mock-file'))).toBeTruthy(); + const resultDir = path.resolve( + __dirname, + `../../../../static/docs/${mockEntity.kind}/default/${mockEntity.metadata.name}`, + ); + + expect(fs.existsSync(resultDir)).toBeTruthy(); + expect(fs.existsSync(path.join(resultDir, '/mock-file'))).toBeTruthy(); fs.removeSync(publishDir); fs.removeSync(tempDir); diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts index 227ba445ae..e33c6fa17d 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts @@ -13,12 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PublisherBase } from './types'; -import { Entity } from '@backstage/catalog-model'; -import path from 'path'; import fs from 'fs-extra'; +import { Logger } from 'winston'; +import { Entity } from '@backstage/catalog-model'; +import { PublisherBase } from './types'; +import { resolvePackagePath } from '@backstage/backend-common'; export class LocalPublish implements PublisherBase { + private readonly logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } + publish({ entity, directory, @@ -30,18 +37,29 @@ export class LocalPublish implements PublisherBase { remoteUrl: string; }> | { remoteUrl: string } { - const publishDir = path.resolve( - __dirname, - `../../../../static/docs/${entity.metadata.name}`, + const entityNamespace = entity.metadata.namespace ?? 'default'; + + const publishDir = resolvePackagePath( + '@backstage/plugin-techdocs-backend', + 'static/docs', + entity.kind, + entityNamespace, + entity.metadata.name, ); if (!fs.existsSync(publishDir)) { + this.logger.info( + `[TechDocs]: Could not find ${publishDir}, creates the directory.`, + ); fs.mkdirSync(publishDir, { recursive: true }); } return new Promise((resolve, reject) => { fs.copy(directory, publishDir, err => { if (err) { + this.logger.debug( + `[TechDocs]: Failed to copy docs from ${directory} to ${publishDir}`, + ); reject(err); } diff --git a/plugins/techdocs/README.md b/plugins/techdocs/README.md index b763679a1c..2e9365f6c6 100644 --- a/plugins/techdocs/README.md +++ b/plugins/techdocs/README.md @@ -1,49 +1,15 @@ # TechDocs Plugin -Welcome to the TechDocs plugin - Spotify's docs-like-code approach built directly into [Backstage](https://backstage.io). Watch [a video of our approach on YouTube](https://www.youtube.com/watch?v=uFGCaZmA6d4) to learn more. - -**WIP: This plugin is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).** - -## Sections - -- [MkDocs](./mkdocs/README.md) - ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/docs](http://localhost:3000/docs). - -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +Set up Backstage and TechDocs by follow our guide on [Getting Started](../../docs/features/techdocs/getting-started.md). ## Configuration ### Custom Storage URL -TechDocs currently reads a static HTML file, generated by Mkdocs (see our `packages/techdocs-container` folder for more documentation) and stored on an external server, and loads that into Backstage. By default, we have set up a mock server with some example documentation sites over in Google Cloud Storage: +TechDocs will try to read your documentation from the URL you have specified in the `techdocs storageUrl` in `app-config.yml`. -```md -# Base URL +### TechDocs Storage Api -https://techdocs-mock-sites.storage.googleapis.com - -# Home Page for the "mkdocs" docs - -https://techdocs-mock-sites.storage.googleapis.com/mkdocs/index.html - -# Home Page for the "backstage-microsite" docs - -https://techdocs-mock-sites.storage.googleapis.com/backstage-microsite/index.html -``` - -Using your own setup (or ours which is being worked on as of Q3 2020), you can point it to your own server with your own hosted documentation sites. The only requirement is that it the output is from [Mkdocs](https://mkdocs.org) with the Material theme. You can always use our documentation generation tool located at `packages/techdocs-container` for easy setup. - -To point TechDocs to your own server, simply update the `techdocs.storageUrl` value in your `app-config.yaml` file or set the environment variable `APP_CONFIG_techdocs_storageUrl` in your application: - -```bash -git clone git@github.com:spotify/backstage.git -cd backstage/ -yarn install -export APP_CONFIG_techdocs_storageUrl='"http://example-docs-site-server.com"' -yarn start -``` +The default setup of TechDocs assumes your documentation is accessed by reading a page with the format of `///`. If for some reason you want to change this it can be configured by implementing a new techdocs storage API. Do this by implementing TechDocsStorage found in `plugins/techdocs/src/api`. Add your new API to the application in `app/src/apis.ts` (or replace if it's already registered as an API). diff --git a/plugins/techdocs/dev/api.ts b/plugins/techdocs/dev/api.ts new file mode 100644 index 0000000000..60a17c3dc3 --- /dev/null +++ b/plugins/techdocs/dev/api.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ParsedEntityId } from '../src/types'; + +import { TechDocsStorage } from '../src/api'; + +export class TechDocsDevStorageApi implements TechDocsStorage { + public apiOrigin: string; + + constructor({ apiOrigin }: { apiOrigin: string }) { + this.apiOrigin = apiOrigin; + } + + async getEntityDocs(entityId: ParsedEntityId, path: string) { + const { name } = entityId; + + const url = `${this.apiOrigin}/${name}/${path}`; + + const request = await fetch( + `${url.endsWith('/') ? url : `${url}/`}index.html`, + ); + + if (request.status === 404) { + throw new Error('Page not found'); + } + + return request.text(); + } + + getBaseUrl( + oldBaseUrl: string, + entityId: ParsedEntityId, + path: string, + ): string { + const { name } = entityId; + return new URL(oldBaseUrl, `${this.apiOrigin}/${name}/${path}`).toString(); + } +} diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 812a5585d4..b415256fde 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -16,5 +16,17 @@ import { createDevApp } from '@backstage/dev-utils'; import { plugin } from '../src/plugin'; +import { TechDocsDevStorageApi } from './api'; +import { techdocsStorageApiRef } from '../src'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerApiFactory({ + deps: {}, + factory: () => + new TechDocsDevStorageApi({ + apiOrigin: 'http://localhost:3000/api', + }), + implements: techdocsStorageApiRef, + }) + .registerPlugin(plugin) + .render(); diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index a1b7ddb589..2d63c9dd53 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/test-utils": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/core-api": "^0.1.1-alpha.21", + "@backstage/plugin-catalog": "^0.1.1-alpha.21", + "@backstage/test-utils": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", @@ -36,13 +40,14 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", + "canvas": "^2.6.1", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/techdocs/src/api.test.ts b/plugins/techdocs/src/api.test.ts new file mode 100644 index 0000000000..0ab05d5ecd --- /dev/null +++ b/plugins/techdocs/src/api.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TechDocsStorageApi } from './api'; + +const DOC_STORAGE_URL = 'https://example-storage.com'; + +const mockEntity = { + kind: 'Component', + namespace: 'default', + name: 'test-component', +}; + +describe('TechDocsStorageApi', () => { + it('should return correct base url based on defined storage', () => { + const storageApi = new TechDocsStorageApi({ apiOrigin: DOC_STORAGE_URL }); + + expect(storageApi.getBaseUrl('test.js', mockEntity, '')).toEqual( + `${DOC_STORAGE_URL}/${mockEntity.kind}/${mockEntity.namespace}/${mockEntity.name}/test.js`, + ); + }); + + it('should return base url with correct entity structure', () => { + const storageApi = new TechDocsStorageApi({ apiOrigin: DOC_STORAGE_URL }); + + expect(storageApi.getBaseUrl('test/', mockEntity, '')).toEqual( + `${DOC_STORAGE_URL}/${mockEntity.kind}/${mockEntity.namespace}/${mockEntity.name}/test/`, + ); + }); +}); diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts new file mode 100644 index 0000000000..951ad5e16c --- /dev/null +++ b/plugins/techdocs/src/api.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core'; + +import { ParsedEntityId } from './types'; + +export const techdocsStorageApiRef = createApiRef({ + id: 'plugin.techdocs.storageservice', + description: 'Used to make requests towards the techdocs storage', +}); + +export interface TechDocsStorage { + getEntityDocs(entityId: ParsedEntityId, path: string): Promise; + getBaseUrl( + oldBaseUrl: string, + entityId: ParsedEntityId, + path: string, + ): string; +} + +export class TechDocsStorageApi implements TechDocsStorage { + public apiOrigin: string; + + constructor({ apiOrigin }: { apiOrigin: string }) { + this.apiOrigin = apiOrigin; + } + + async getEntityDocs(entityId: ParsedEntityId, path: string) { + const { kind, namespace, name } = entityId; + + const url = `${this.apiOrigin}/${kind}/${namespace ? namespace : 'default'}/${name}/${path}`; + + const request = await fetch( + `${url.endsWith('/') ? url : `${url}/`}index.html`, + ); + + if (request.status === 404) { + throw new Error('Page not found'); + } + + return request.text(); + } + + getBaseUrl( + oldBaseUrl: string, + entityId: ParsedEntityId, + path: string, + ): string { + const { kind, namespace, name } = entityId; + + return new URL( + oldBaseUrl, + `${this.apiOrigin}/${kind}/${namespace ? namespace : 'default'}/${name}/${path}`, + ).toString(); + } +} diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 3a0a0fe2d3..2a412d1b20 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -15,3 +15,5 @@ */ export { plugin } from './plugin'; +export * from './reader'; +export * from './api'; diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index a96f7e5502..9b09544427 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -31,7 +31,7 @@ import { createPlugin, createRouteRef } from '@backstage/core'; import { TechDocsHome } from './reader/components/TechDocsHome'; -import { Reader } from './reader/components/Reader'; +import { TechDocsPage } from './reader/components/TechDocsPage'; export const rootRouteRef = createRouteRef({ path: '/docs', @@ -39,7 +39,7 @@ export const rootRouteRef = createRouteRef({ }); export const rootDocsRouteRef = createRouteRef({ - path: '/docs/:componentId/*', + path: '/docs/:entityId/*', title: 'Docs', }); @@ -47,6 +47,6 @@ export const plugin = createPlugin({ id: 'techdocs', register({ router }) { router.addRoute(rootRouteRef, TechDocsHome); - router.addRoute(rootDocsRouteRef, Reader); + router.addRoute(rootDocsRouteRef, TechDocsPage); }, }); diff --git a/plugins/techdocs/src/reader/README.md b/plugins/techdocs/src/reader/README.md new file mode 100644 index 0000000000..fb9f613651 --- /dev/null +++ b/plugins/techdocs/src/reader/README.md @@ -0,0 +1,20 @@ +# TechDocs Reader + +The TechDocs reader is a component that fetches a remote page, runs transformers on it and renders it into a shadow dom. + +Currently there's no easy way to customize which transformers to run or add new ones. If that is needed you would have to fork the techdocs plugin and make your changes in that fork. + +Transformers are functions that optionally takes in parameters from the Reader.tsx component and returns a function which gets passed the DOM of the fetched page. A very simple transformer can look like this. + +```typescript +export const updateH1Text = (): Transformer => { + return dom => { + // Change the first occurance of H1 to say "TechDocs!" + dom.querySelector('h1')?.innerHTML = 'TechDocs!'; + + return dom; + }; +}; +``` + +The transformers are then registered in the Reader.tsx file. They are registered in two places, one place that runs before it's attached to the actual browser DOM (preTransformers) and once after (postTransfomers). Doing modifications is faster before it's attached, but doesn't allow us to do some things, such as attaching event listeners. diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 8e11df428e..8ae3273e12 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -15,12 +15,12 @@ */ import React from 'react'; -import { useApi, configApiRef } from '@backstage/core'; +import { useApi, Progress } from '@backstage/core'; import { useShadowDom } from '..'; import { useAsync } from 'react-use'; -import { AsyncState } from 'react-use/lib/useAsync'; - -import { useLocation, useParams, useNavigate } from 'react-router-dom'; +import { techdocsStorageApiRef } from '../../api'; +import { useParams, useNavigate } from 'react-router-dom'; +import { ParsedEntityId } from '../../types'; import transformer, { addBaseUrl, @@ -31,76 +31,35 @@ import transformer, { onCssReady, sanitizeDOM, } from '../transformers'; -import URLFormatter from '../urlFormatter'; import { TechDocsNotFound } from './TechDocsNotFound'; -import { TechDocsPageWrapper } from './TechDocsPageWrapper'; -const useFetch = (url: string): AsyncState => { - const state = useAsync(async () => { - const request = await fetch(url); - if (request.status === 404) { - return [request.url, new Error('Page not found')]; - } - const response = await request.text(); - return [request.url, response]; - }, [url]); - - const [fetchedUrl, fetchedValue] = state.value ?? []; - - if (url !== fetchedUrl) { - // Fixes a race condition between two pages - return { loading: true }; - } - - return Object.assign(state, fetchedValue ? { value: fetchedValue } : {}); +type Props = { + entityId: ParsedEntityId; }; -const useEnforcedTrailingSlash = (): void => { - React.useEffect(() => { - const actualUrl = window.location.href; - const expectedUrl = new URLFormatter(window.location.href).formatBaseURL(); +export const Reader = ({ entityId }: Props) => { + const { kind, namespace, name } = entityId; + const { '*': path } = useParams(); - if (actualUrl !== expectedUrl) { - window.history.replaceState({}, document.title, expectedUrl); - } - }, []); -}; - -export const Reader = () => { - useEnforcedTrailingSlash(); - - const docStorageUrl = - useApi(configApiRef).getOptionalString('techdocs.storageUrl') ?? - 'https://techdocs-mock-sites.storage.googleapis.com'; - - const location = useLocation(); - const { componentId, '*': path } = useParams(); + const techdocsStorageApi = useApi(techdocsStorageApiRef); const [shadowDomRef, shadowRoot] = useShadowDom(); const navigate = useNavigate(); - const normalizedUrl = new URLFormatter( - `${docStorageUrl}${location.pathname.replace('/docs', '')}`, - ).formatBaseURL(); - const state = useFetch(`${normalizedUrl}index.html`); + + const { value, loading, error } = useAsync(async () => { + return techdocsStorageApi.getEntityDocs({ kind, namespace, name }, path); + }, [techdocsStorageApi, kind, namespace, name, path]); React.useEffect(() => { - if (!shadowRoot) { - return; // Shadow DOM isn't ready - } - - if (state.loading) { - return; // Page isn't ready - } - - if (state.value instanceof Error) { - return; // Docs not found + if (!shadowRoot || loading || error) { + return; // Shadow DOM isn't ready / It's not ready / Docs was not found } // Pre-render - const transformedElement = transformer(state.value as string, [ + const transformedElement = transformer(value as string, [ sanitizeDOM(), addBaseUrl({ - docStorageUrl, - componentId, + techdocsStorageApi, + entityId: entityId, path, }), rewriteDocLinks(), @@ -145,7 +104,7 @@ export const Reader = () => { }, }), onCssReady({ - docStorageUrl, + docStorageUrl: techdocsStorageApi.apiOrigin, onLoading: (dom: Element) => { (dom as HTMLElement).style.setProperty('opacity', '0'); }, @@ -154,15 +113,28 @@ export const Reader = () => { }, }), ]); - }, [componentId, path, shadowRoot, state]); // eslint-disable-line react-hooks/exhaustive-deps + }, [ + name, + path, + shadowRoot, + value, + error, + loading, + namespace, + kind, + entityId, + navigate, + techdocsStorageApi, + ]); - if (state.value instanceof Error) { + if (error) { return ; } return ( - + <> + {loading ? : null}
- + ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx index dd31e6a318..7754f227fc 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx @@ -17,20 +17,34 @@ import { TechDocsHome } from './TechDocsHome'; import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; +import { ApiRegistry, ApiProvider } from '@backstage/core-api'; + +import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; describe('TechDocs Home', () => { - it('should render a TechDocs home page', () => { - const { getByTestId, queryByText } = render( - wrapInTestApp(), + const catalogApi: Partial = { + getEntities: () => Promise.resolve([] as Entity[]), + }; + + const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]); + + it('should render a TechDocs home page', async () => { + const { findByTestId, findByText } = render( + wrapInTestApp( + + + , + ), ); // Header - expect(queryByText('Documentation')).toBeInTheDocument(); + expect(await findByText('Documentation')).toBeInTheDocument(); expect( - queryByText(/Documentation available in Backstage/i), + await findByText(/Documentation available in Backstage/i), ).toBeInTheDocument(); // Explore Content - expect(getByTestId('docs-explore')).toBeDefined(); + expect(await findByTestId('docs-explore')).toBeDefined(); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index f8f1e89b62..17b9fd36f7 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -15,59 +15,71 @@ */ import React from 'react'; +import { useAsync } from 'react-use'; import { useNavigate } from 'react-router-dom'; import { Grid } from '@material-ui/core'; -import { ItemCard } from '@backstage/core'; +import { ItemCard, Progress, useApi } from '@backstage/core'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; +import { catalogApiRef } from '@backstage/plugin-catalog'; -type DocumentationSite = { - title: string; - description: string; - tags: Array; - path: string; - btnLabel: string; -}; - -const documentationSites: Array = [ - { - title: 'MkDocs', - description: - "MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. ", - tags: ['Developer Tool'], - path: '/docs/mkdocs', - btnLabel: 'Read Docs', - }, - { - title: 'Backstage Docs', - description: 'Main documentation for Backstage features and platform APIs.', - tags: ['Service'], - path: '/docs/backstage', - btnLabel: 'Read Docs', - }, -]; export const TechDocsHome = () => { + const catalogApi = useApi(catalogApiRef); const navigate = useNavigate(); - return ( - <> + const { value, loading, error } = useAsync(async () => { + const entities = await catalogApi.getEntities(); + return entities.filter(entity => { + return !!entity.metadata.annotations?.['backstage.io/techdocs-ref']; + }); + }); + + if (loading) { + return ( - - {documentationSites.map((site: DocumentationSite, index: number) => ( - - navigate(site.path)} - tags={site.tags} - title={site.title} - label={site.btnLabel} - description={site.description} - /> - - ))} - + - + ); + } + + if (error) { + return ( + +

{error.message}

+
+ ); + } + + return ( + + + {value?.length + ? value.map((entity, index: number) => ( + + + navigate( + `/docs/${entity.kind}:${ + entity.metadata.namespace ?? '' + }:${entity.metadata.name}`, + ) + } + title={entity.metadata.name} + label="Read Docs" + description={entity.metadata.description} + /> + + )) + : null} + + ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.tsx new file mode 100644 index 0000000000..1328d13ee9 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsPage.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; +import { Reader } from './Reader'; + +export const TechDocsPage = () => { + const { entityId } = useParams(); + + const [kind, namespace, name] = entityId.split(':'); + + return ( + + + + ); +}; diff --git a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx index d4cc49c919..5265492fb3 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import { TechDocsPageWrapper } from './TechDocsPageWrapper'; -import { TechDocsHome } from './TechDocsHome'; import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; @@ -24,7 +23,7 @@ describe('TechDocs Page Wrapper', () => { const { queryByText } = render( wrapInTestApp( - + Test , ), ); diff --git a/plugins/techdocs/src/reader/components/index.ts b/plugins/techdocs/src/reader/components/index.ts new file mode 100644 index 0000000000..b14a6c5b84 --- /dev/null +++ b/plugins/techdocs/src/reader/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Reader'; diff --git a/plugins/techdocs/src/reader/index.tsx b/plugins/techdocs/src/reader/index.tsx index 72b51bd0dc..a3ca8f394b 100644 --- a/plugins/techdocs/src/reader/index.tsx +++ b/plugins/techdocs/src/reader/index.tsx @@ -15,3 +15,4 @@ */ export * from './hooks'; +export * from './components'; diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index ce903c5d2f..728c39826d 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -14,124 +14,65 @@ * limitations under the License. */ -import { createTestShadowDom, FIXTURES, getSample } from '../../test-utils'; +import { createTestShadowDom } from '../../test-utils'; import { addBaseUrl } from '../transformers'; +import { TechDocsStorage } from '../../api'; const DOC_STORAGE_URL = 'https://example-host.storage.googleapis.com'; +const techdocsStorageApi: TechDocsStorage = { + getBaseUrl: jest.fn(() => DOC_STORAGE_URL), + getEntityDocs: () => new Promise(resolve => resolve('yes!')), +}; + +const fixture = ` + + + + + + + + + +`; + +const mockEntityId = { + kind: '', + namespace: '', + name: '', +}; + describe('addBaseUrl', () => { it('contains relative paths', () => { - const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE); - - expect(getSample(shadowDom, 'img', 'src')).toEqual([ - 'img/win-py-install.png', - 'img/initial-layout.png', - ]); - expect(getSample(shadowDom, 'link', 'href')).toEqual([ - 'https://www.mkdocs.org/', - 'assets/images/favicon.png', - ]); - expect(getSample(shadowDom, 'script', 'src')).toEqual([ - 'https://www.google-analytics.com/analytics.js', - 'assets/javascripts/vendor.d710d30a.min.js', - ]); - }); - - it('contains transformed absolute paths', () => { - const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + createTestShadowDom(fixture, { preTransformers: [ addBaseUrl({ - docStorageUrl: DOC_STORAGE_URL, - componentId: 'example-docs', + techdocsStorageApi, + entityId: mockEntityId, path: '', }), ], postTransformers: [], }); - expect(getSample(shadowDom, 'img', 'src')).toEqual([ - 'https://example-host.storage.googleapis.com/example-docs/img/win-py-install.png', - 'https://example-host.storage.googleapis.com/example-docs/img/initial-layout.png', - ]); - expect(getSample(shadowDom, 'link', 'href')).toEqual([ - 'https://www.mkdocs.org/', - 'https://example-host.storage.googleapis.com/example-docs/assets/images/favicon.png', - ]); - expect(getSample(shadowDom, 'script', 'src')).toEqual([ - 'https://www.google-analytics.com/analytics.js', - 'https://example-host.storage.googleapis.com/example-docs/assets/javascripts/vendor.d710d30a.min.js', - ]); - }); - - it('includes path option without slash', () => { - const shadowDom = createTestShadowDom( - ` - - - - - - - `, - { - preTransformers: [ - addBaseUrl({ - docStorageUrl: DOC_STORAGE_URL, - componentId: 'example-docs', - path: 'examplepath', - }), - ], - postTransformers: [], - }, + expect(techdocsStorageApi.getBaseUrl).toHaveBeenNthCalledWith( + 1, + 'test.jpg', + mockEntityId, + '', ); - - expect(getSample(shadowDom, 'img', 'src')).toEqual([ - 'https://example-host.storage.googleapis.com/example-docs/img/win-py-install.png', - 'https://example-host.storage.googleapis.com/example-docs/img/initial-layout.png', - ]); - expect(getSample(shadowDom, 'link', 'href')).toEqual([ - 'https://www.mkdocs.org/', - 'https://example-host.storage.googleapis.com/example-docs/assets/images/favicon.png', - ]); - expect(getSample(shadowDom, 'script', 'src')).toEqual([ - 'https://www.google-analytics.com/analytics.js', - 'https://example-host.storage.googleapis.com/example-docs/assets/javascripts/vendor.d710d30a.min.js', - ]); - }); - - it('includes path option with slash', () => { - const shadowDom = createTestShadowDom( - ` - - - - - - - `, - { - preTransformers: [ - addBaseUrl({ - docStorageUrl: DOC_STORAGE_URL, - componentId: 'example-docs', - path: 'examplepath/', - }), - ], - postTransformers: [], - }, + expect(techdocsStorageApi.getBaseUrl).toHaveBeenNthCalledWith( + 2, + 'script.js', + mockEntityId, + '', + ); + expect(techdocsStorageApi.getBaseUrl).toHaveBeenNthCalledWith( + 3, + 'astyle.css', + mockEntityId, + '', ); - - expect(getSample(shadowDom, 'img', 'src')).toEqual([ - 'https://example-host.storage.googleapis.com/example-docs/img/win-py-install.png', - 'https://example-host.storage.googleapis.com/example-docs/img/initial-layout.png', - ]); - expect(getSample(shadowDom, 'link', 'href')).toEqual([ - 'https://www.mkdocs.org/', - 'https://example-host.storage.googleapis.com/example-docs/assets/images/favicon.png', - ]); - expect(getSample(shadowDom, 'script', 'src')).toEqual([ - 'https://www.google-analytics.com/analytics.js', - 'https://example-host.storage.googleapis.com/example-docs/assets/javascripts/vendor.d710d30a.min.js', - ]); }); }); diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index 42ed531aea..7346ca7ce2 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -14,18 +14,19 @@ * limitations under the License. */ -import URLFormatter from '../urlFormatter'; import type { Transformer } from './index'; +import { TechDocsStorage } from '../../api'; +import { ParsedEntityId } from '../../types'; type AddBaseUrlOptions = { - docStorageUrl: string; - componentId: string; + techdocsStorageApi: TechDocsStorage; + entityId: ParsedEntityId; path: string; }; export const addBaseUrl = ({ - docStorageUrl, - componentId, + techdocsStorageApi, + entityId, path, }: AddBaseUrlOptions): Transformer => { return dom => { @@ -36,15 +37,11 @@ export const addBaseUrl = ({ Array.from(list) .filter(elem => !!elem.getAttribute(attributeName)) .forEach((elem: T) => { - const urlFormatter = new URLFormatter( - path.length < 1 || path.endsWith('/') - ? `${docStorageUrl}/${componentId}/${path}` - : `${docStorageUrl}/${componentId}/${path}/`, - ); - + const elemAttribute = elem.getAttribute(attributeName); + if (!elemAttribute) return; elem.setAttribute( attributeName, - urlFormatter.formatURL(elem.getAttribute(attributeName)!), + techdocsStorageApi.getBaseUrl(elemAttribute, entityId, path), ); }); }; diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts index 13965bcf4b..c83a94f35d 100644 --- a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts +++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts @@ -15,19 +15,23 @@ */ import { - FIXTURES, createTestShadowDom, mockStylesheetEventListener, executeStylesheetEventListeners, clearStylesheetEventListeners, } from '../../test-utils'; -import { addBaseUrl, onCssReady } from '../transformers'; +import { onCssReady } from '../transformers'; const docStorageUrl: string = 'https://techdocs-mock-sites.storage.googleapis.com'; jest.useFakeTimers(); +const fixture = ` + + +`; + describe('onCssReady', () => { beforeEach(() => { mockStylesheetEventListener(100); @@ -37,19 +41,13 @@ describe('onCssReady', () => { clearStylesheetEventListeners(); }); - it('does not call onLoading and onLoaded without the addBaseUrl transformer', () => { + it('does not call onLoading and onLoaded without the onCssReady transformer', () => { const onLoading = jest.fn(); const onLoaded = jest.fn(); - createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + createTestShadowDom(fixture, { preTransformers: [], - postTransformers: [ - onCssReady({ - docStorageUrl, - onLoading, - onLoaded, - }), - ], + postTransformers: [], }); expect(onLoading).not.toHaveBeenCalled(); @@ -61,14 +59,9 @@ describe('onCssReady', () => { const onLoading = jest.fn(); const onLoaded = jest.fn(); - createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + createTestShadowDom(fixture, { preTransformers: [], postTransformers: [ - addBaseUrl({ - docStorageUrl, - componentId: 'mkdocs', - path: '', - }), onCssReady({ docStorageUrl, onLoading, diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 20ec3ccb64..06da8a447c 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -50,9 +50,9 @@ describe('rewriteDocLinks', () => { expect(getSample(shadowDom, 'a', 'href', 6)).toEqual([ 'http://example.org/', - 'http://localhost/example/', - 'http://localhost/example-docs/', - 'http://localhost/example-docs/example-page/', + 'http://localhost/example', + 'http://localhost/example-docs', + 'http://localhost/example-docs/example-page', ]); }); }); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index 4bf4ab3898..856bb40f0f 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import URLFormatter from '../urlFormatter'; import type { Transformer } from './index'; export const rewriteDocLinks = (): Transformer => { @@ -26,11 +25,17 @@ export const rewriteDocLinks = (): Transformer => { Array.from(list) .filter(elem => elem.hasAttribute(attributeName)) .forEach((elem: T) => { - const urlFormatter = new URLFormatter(window.location.href); - elem.setAttribute( - attributeName, - urlFormatter.formatURL(elem.getAttribute(attributeName)!), - ); + const elemAttribute = elem.getAttribute(attributeName); + if (elemAttribute) { + const normalizedWindowLocation = window.location.href.endsWith('/') + ? window.location.href + : `${window.location.href}/`; + + elem.setAttribute( + attributeName, + new URL(elemAttribute, normalizedWindowLocation).toString(), + ); + } }); }; diff --git a/plugins/techdocs/src/reader/urlFormatter.test.ts b/plugins/techdocs/src/reader/urlFormatter.test.ts deleted file mode 100644 index 2659f9f4dc..0000000000 --- a/plugins/techdocs/src/reader/urlFormatter.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import URLFormatter from './urlFormatter'; - -describe('URLFormatter', () => { - describe('formatURL', () => { - it('should not change an absolute url', () => { - const formatter = new URLFormatter('https://www.google.com/'); - expect(formatter.formatURL('https://www.mkdocs.org/')).toEqual( - 'https://www.mkdocs.org/', - ); - }); - - it('should convert a relative url to an absolute url', () => { - const formatter = new URLFormatter( - 'https://www.mkdocs.org/user-guide/getting-started/', - ); - expect(formatter.formatURL('../../support/installing/')).toEqual( - 'https://www.mkdocs.org/support/installing/', - ); - }); - - it('should add a trailing slash', () => { - const formatter = new URLFormatter( - 'https://www.mkdocs.org/user-guide/getting-started', - ); - expect(formatter.formatURL('./getting-started')).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/', - ); - }); - - it('should not add a trailing slash', () => { - const formatter = new URLFormatter( - 'https://www.mkdocs.org/user-guide/getting-started/', - ); - expect(formatter.formatURL('.')).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/', - ); - }); - - it('should not add multiple hashes', () => { - const formatter = new URLFormatter( - 'https://www.mkdocs.org/user-guide/getting-started/#hash1', - ); - expect(formatter.formatURL('./#hash2')).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/#hash2', - ); - }); - }); - - describe('formatBaseURL', () => { - it('should keep query params in URL', () => { - const formatter = new URLFormatter( - 'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world', - ); - expect(formatter.formatBaseURL()).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world', - ); - }); - - it('should keep hash in URL', () => { - const formatter = new URLFormatter( - 'https://www.mkdocs.org/user-guide/getting-started/#hash', - ); - expect(formatter.formatBaseURL()).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/#hash', - ); - }); - }); -}); diff --git a/plugins/techdocs/src/reader/urlFormatter.ts b/plugins/techdocs/src/reader/urlFormatter.ts deleted file mode 100644 index 166f0d9963..0000000000 --- a/plugins/techdocs/src/reader/urlFormatter.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default class URLFormatter { - constructor(public baseURL: string) {} - - formatBaseURL(): string { - return this.normalizeURL(this.baseURL); - } - - formatURL(pathname: string): string { - return this.normalizeURL(new URL(pathname, this.baseURL).toString()); - } - - private normalizeURL(urlString: string): string { - const url = new URL(urlString); - const filename: string = url.pathname.split('/').pop() ?? url.pathname; - const isDir: boolean = filename.includes('.') === false; - - if (isDir) { - url.pathname = url.pathname.replace(/([^/])$/, '$1/'); - } - - return url.toString(); - } -} diff --git a/plugins/techdocs/src/types.ts b/plugins/techdocs/src/types.ts new file mode 100644 index 0000000000..ec907cc6fa --- /dev/null +++ b/plugins/techdocs/src/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type ParsedEntityId = { + kind: string; + namespace?: string; + name: string; +}; diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 2c0c838de8..2d754a51a6 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.18", + "version": "0.1.1-alpha.21", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.18", - "@backstage/theme": "^0.1.1-alpha.18", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.18", - "@backstage/dev-utils": "^0.1.1-alpha.18", + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/tsconfig.json b/tsconfig.json index 1a6128b5c9..7ea674798c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "plugins/*/migrations" ], "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "rootDir": "." } } diff --git a/yarn.lock b/yarn.lock index f873b2cdaf..4d544e2275 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,15 @@ # yarn lockfile v1 +"@apidevtools/json-schema-ref-parser@9.0.6", "@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.0.6" + resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c" + integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + js-yaml "^3.13.1" + "@apollo/protobufjs@^1.0.3": version "1.0.4" resolved "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.0.4.tgz#cf01747a55359066341f31b5ce8db17df44244e0" @@ -40,6 +49,45 @@ resolved "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.1.tgz#1403ac5de10d8ca689fc1f65844c27179ae1d44f" integrity sha512-UQ9BequOTIavs0pTHLMwQwKQF8tTV1oezY/H2O9chA+JNPFZSua55xpU5dPSjAU9/jLJ1VwU+HJuTVN8u7S6Fg== +"@asyncapi/avro-schema-parser@^0.1.2": + version "0.1.2" + resolved "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-0.1.2.tgz#f6c340ccaa24bc36399d3a0f1a6e02790a948453" + integrity sha512-K4GlakiE42J9AWwAu3BWn3Qbf+N8C6vE4eEm5LEx7HqAhHG+FA0U7/3vvXoj31rnS/8sgPQpc2msLWQlmCtZyw== + +"@asyncapi/openapi-schema-parser@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@asyncapi/openapi-schema-parser/-/openapi-schema-parser-2.0.0.tgz#80e2f38e92b6635dde19aae07b92e3caa0effc58" + integrity sha512-XfDp3EIs6ptar3jARQZzi3ObmS44l6Qozc5GJmZJUQ6mHLTTqUGJ0nxcrXAW88vosjilgJVaQ63oGolA6smSHQ== + dependencies: + "@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0" + +"@asyncapi/parser@^1.0.0-rc.1": + version "1.0.0-rc.2" + resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.0.0-rc.2.tgz#87d2e83c1d390e21d53f868e6209cbc584d35449" + integrity sha512-nZYJLnMiq48q7YHa+AI9ZaDc5UqoqKR7MDhTiIKsIJj6X5fF6hpwBrTP0bQn76Pc1oGBXRB69PcD/Y+Et1qFyw== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@asyncapi/specs" "^2.7.4" + "@fmvilas/pseudo-yaml-ast" "^0.3.1" + ajv "^6.10.1" + js-yaml "^3.13.1" + json-to-ast "^2.1.0" + node-fetch "^2.6.0" + tiny-merge-patch "^0.1.2" + +"@asyncapi/raml-dt-schema-parser@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@asyncapi/raml-dt-schema-parser/-/raml-dt-schema-parser-2.0.0.tgz#6e9eff89032aa7b82a963d8e72e320e493fc3835" + integrity sha512-ve41LIvbqDU8s0ZeDrWy+dyQZ/XSb7vKiJUHNgy8xcgxsz4YuttbEJbyGJvarw3qf2y+0y6cA1KUFiXoE+AIUg== + dependencies: + js-yaml "^3.13.1" + ramldt2jsonschema "^1.1.0" + +"@asyncapi/specs@^2.7.4": + version "2.7.4" + resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.4.tgz#5db4390b68aeb40d70c1723d8f46b0a091110afc" + integrity sha512-3Np9ip1Qn5AgnxTrbI0CMW2F/WUorpiAKz+GUfEy4a6GPk0eTSI6CIcbx2/jej5P91nhEyny9+D3oIzn2MreTA== + "@babel/code-frame@7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" @@ -54,70 +102,60 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": +"@babel/code-frame@7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== dependencies: "@babel/highlight" "^7.8.3" -"@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.5.5": version "7.10.4" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.5.tgz#d38425e67ea96b1480a3f50404d1bf85676301a6" - integrity sha512-mPVoWNzIpYJHbWje0if7Ck36bpbtTvIxOi9+6WSK9wjGEXearAqlwBoTQvVjsAY2VIwgcs8V940geY3okzRCEw== +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" + integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== dependencies: browserslist "^4.12.0" invariant "^2.2.4" semver "^5.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.4.5", "@babel/core@^7.7.5": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz#d9aa1f580abf3b2286ef40b6904d390904c63376" - integrity sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg== +"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.4.5", "@babel/core@^7.7.5", "@babel/core@^7.9.0": + version "7.11.1" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" + integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.6" - "@babel/parser" "^7.9.6" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.0" + "@babel/types" "^7.11.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" json5 "^2.1.2" - lodash "^4.17.13" + lodash "^4.17.19" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" - integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== +"@babel/generator@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" + integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== dependencies: - "@babel/types" "^7.10.5" + "@babel/types" "^7.11.0" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43" - integrity sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ== - dependencies: - "@babel/types" "^7.9.6" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -125,13 +163,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" @@ -180,18 +211,6 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" -"@babel/helper-create-class-features-plugin@^7.8.3": - version "7.8.6" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz#243a5b46e2f8f0f674dc1387631eb6b28b851de0" - integrity sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -201,15 +220,6 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - regexpu-core "^4.7.0" - "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -236,15 +246,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.8.3", "@babel/helper-function-name@^7.9.5": - version "7.9.5" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" - integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.9.5" - "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -252,13 +253,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" - integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -273,53 +267,26 @@ dependencies: "@babel/types" "^7.10.5" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" - integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.10.4": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6" - integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== dependencies: "@babel/helper-module-imports" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" "@babel/template" "^7.10.4" - "@babel/types" "^7.10.5" + "@babel/types" "^7.11.0" lodash "^4.17.19" -"@babel/helper-module-transforms@^7.9.0": - version "7.9.0" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" - integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== - dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-simple-access" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.6" - "@babel/types" "^7.9.0" - lodash "^4.17.13" - "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -327,19 +294,7 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== - -"@babel/helper-plugin-utils@^7.10.4": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== @@ -351,13 +306,6 @@ dependencies: lodash "^4.17.19" -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== - dependencies: - lodash "^4.17.13" - "@babel/helper-remap-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" @@ -379,16 +327,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" - "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -397,38 +335,25 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-simple-access@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" - integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== +"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" + integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== dependencies: - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" - integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-split-export-declaration@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" - integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== - dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.11.0" "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5": - version "7.9.5" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" - integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== - "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -439,25 +364,16 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz#092c774743471d0bb6c7de3ad465ab3d3486d580" - integrity sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw== +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/highlight@^7.0.0", "@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.10.4": +"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== @@ -466,15 +382,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz#3b1bbb30dabe600cd72db58720998376ff653bc7" - integrity sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q== - -"@babel/parser@^7.10.4", "@babel/parser@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" - integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.7.5": + version "7.11.3" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" + integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== "@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" @@ -485,7 +396,7 @@ "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@^7.10.4": +"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.7.0", "@babel/plugin-proposal-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== @@ -493,14 +404,6 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-class-properties@^7.7.0": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz#5e06654af5cd04b608915aada9b2a6788004464e" - integrity sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-dynamic-import@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" @@ -509,6 +412,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" + integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-json-strings@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" @@ -517,6 +428,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" + integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" @@ -533,23 +452,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.4.tgz#50129ac216b9a6a55b3853fdd923e74bf553a4c0" - integrity sha512-6vh4SqRuLLarjgeOf4EaROJAHjvu9Gl+/346PbDH9yWbJyfnJ/ah3jmYKYtswEyCoWZiidvVHjHshd4WgjB9BA== +"@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.6.2", "@babel/plugin-proposal-object-rest-spread@^7.9.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" + integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.6.2": - version "7.9.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" - integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-proposal-optional-catch-binding@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" @@ -558,12 +469,13 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.4.tgz#750f1255e930a1f82d8cdde45031f81a0d0adff7" - integrity sha512-ZIhQIEeavTgouyMSdZRap4VPPHqJJ3NEs2cuHs5p0erH+iz6khB0qfgU8g7UuJkG88+fBMy23ZiU+nuHvekJeQ== +"@babel/plugin-proposal-optional-chaining@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" + integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" "@babel/plugin-proposal-private-methods@^7.10.4": @@ -574,7 +486,7 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.4": +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -582,14 +494,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.8.8" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -604,20 +508,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.4": +"@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" - integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -625,6 +522,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-flow@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.8.3.tgz#f2c883bd61a6316f2c89380ae5122f923ba4527f" @@ -632,6 +536,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" @@ -646,12 +557,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" - integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg== +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" @@ -660,20 +571,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" - integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" @@ -760,7 +664,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.10.4": +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -768,14 +672,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-duplicate-keys@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" @@ -837,7 +733,7 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.10.4": +"@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== @@ -847,16 +743,6 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.4.4": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.6.tgz#64b7474a4279ee588cacd1906695ca721687c277" - integrity sha512-7H25fSlLcn+iYimmsNe3uK1at79IE6SKW9q0/QeEHTMC9MdOZ+4bA+T1VFB5fgOqBWoqlifXRzYD0JPdmIrgSQ== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.3" - "@babel/plugin-transform-modules-systemjs@^7.10.4": version "7.10.5" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" @@ -912,7 +798,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-constant-elements@^7.0.0", "@babel/plugin-transform-react-constant-elements@^7.2.0", "@babel/plugin-transform-react-constant-elements@^7.6.3", "@babel/plugin-transform-react-constant-elements@^7.7.4": +"@babel/plugin-transform-react-constant-elements@^7.0.0", "@babel/plugin-transform-react-constant-elements@^7.2.0", "@babel/plugin-transform-react-constant-elements@^7.6.3", "@babel/plugin-transform-react-constant-elements@^7.7.4", "@babel/plugin-transform-react-constant-elements@^7.9.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz#0f485260bf1c29012bb973e7e404749eaac12c9e" integrity sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g== @@ -990,12 +876,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-spread@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.4.tgz#4e2c85ea0d6abaee1b24dcfbbae426fe8d674cff" - integrity sha512-1e/51G/Ni+7uH5gktbWv+eCED9pP8ZpRhZB3jOaI3mmzfvJTWHkuyYTv0Z5PYtyM+Tr2Ccr9kUdQxn60fI5WuQ== +"@babel/plugin-transform-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" + integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-transform-sticky-regex@^7.10.4": version "7.10.4" @@ -1035,30 +922,42 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-env@^7.4.5", "@babel/preset-env@^7.9.5": +"@babel/polyfill@^7.8.7": version "7.10.4" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.10.4.tgz#fbf57f9a803afd97f4f32e4f798bb62e4b2bef5f" - integrity sha512-tcmuQ6vupfMZPrLrc38d0sF2OjLT3/bZ0dry5HchNCQbrokoQi4reXqclvkkAT5b+gWc23meVWpve5P/7+w/zw== + resolved "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.10.4.tgz#915e5bfe61490ac0199008e35ca9d7d151a8e45a" + integrity sha512-8BYcnVqQ5kMD2HXoHInBH7H1b/uP3KdnwCYXOqFnXqguOyuu443WXusbIUbWEfY3Z0Txk0M1uG/8YuAMhNl6zg== dependencies: - "@babel/compat-data" "^7.10.4" + core-js "^2.6.5" + regenerator-runtime "^0.13.4" + +"@babel/preset-env@^7.4.5", "@babel/preset-env@^7.9.0", "@babel/preset-env@^7.9.5": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" + integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + dependencies: + "@babel/compat-data" "^7.11.0" "@babel/helper-compilation-targets" "^7.10.4" "@babel/helper-module-imports" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-proposal-async-generator-functions" "^7.10.4" "@babel/plugin-proposal-class-properties" "^7.10.4" "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" "@babel/plugin-proposal-numeric-separator" "^7.10.4" - "@babel/plugin-proposal-object-rest-spread" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" "@babel/plugin-proposal-private-methods" "^7.10.4" "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" "@babel/plugin-syntax-class-properties" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" @@ -1091,14 +990,14 @@ "@babel/plugin-transform-regenerator" "^7.10.4" "@babel/plugin-transform-reserved-words" "^7.10.4" "@babel/plugin-transform-shorthand-properties" "^7.10.4" - "@babel/plugin-transform-spread" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" "@babel/plugin-transform-sticky-regex" "^7.10.4" "@babel/plugin-transform-template-literals" "^7.10.4" "@babel/plugin-transform-typeof-symbol" "^7.10.4" "@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.10.4" + "@babel/types" "^7.11.0" browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" @@ -1137,15 +1036,26 @@ "@babel/plugin-transform-react-jsx-source" "^7.10.4" "@babel/plugin-transform-react-pure-annotations" "^7.10.4" -"@babel/runtime-corejs2@^7.8.7": - version "7.10.3" - resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.3.tgz#81bc99a96bfcb6db3f0dcf73fdc577cc554d341b" - integrity sha512-enKvnR/kKFbZFgXYo165wtSA5OeiTlgsnU4jV3vpKRhfWUJjLS6dfVcjIPeRcgJbgEgdgu0I+UyBWqu6c0GumQ== +"@babel/register@^7.9.0": + version "7.10.5" + resolved "https://registry.npmjs.org/@babel/register/-/register-7.10.5.tgz#354f3574895f1307f79efe37a51525e52fd38d89" + integrity sha512-eYHdLv43nyvmPn9bfNfrcC4+iYNwdQ8Pxk1MFJuU/U5LpSYl/PH4dFMazCYZDFVi8ueG3shvO+AQfLrxpYulQw== + dependencies: + find-cache-dir "^2.0.0" + lodash "^4.17.19" + make-dir "^2.1.0" + pirates "^4.0.0" + source-map-support "^0.5.16" + +"@babel/runtime-corejs2@^7.10.4", "@babel/runtime-corejs2@^7.8.7": + version "7.11.2" + resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" + integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs3@^7.10.2": +"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.8.3": version "7.10.3" resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== @@ -1153,22 +1063,14 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs3@^7.8.3": - version "7.9.2" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz#26fe4aa77e9f1ecef9b776559bbb8e84d34284b7" - integrity sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" - integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.11.2" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4": +"@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.7.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== @@ -1177,58 +1079,25 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442" - integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.6" - "@babel/types" "^7.9.6" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" - integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.9.0": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" + integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.5" + "@babel/generator" "^7.11.0" "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/parser" "^7.10.5" - "@babel/types" "^7.10.5" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.0" + "@babel/types" "^7.11.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5", "@babel/types@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" - integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.10.4", "@babel/types@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" - integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.9.0", "@babel/types@^7.9.5": + version "7.11.0" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" + integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== dependencies: "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.19" @@ -1239,6 +1108,11 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@braintree/sanitize-url@^4.0.0": + version "4.1.1" + resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-4.1.1.tgz#671b3cfdbcc40d1449036ce586e882ab6150828e" + integrity sha512-epVksusKVEpwBs2vRg3SWssxn9KXs9CxEYNOcgeSRLRjq070ABj5bLPxkmtQpVeSPCHj8kfAE9J6z2WsLr4wZg== + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -1247,7 +1121,7 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@cypress/listr-verbose-renderer@0.4.1": +"@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= @@ -1257,7 +1131,7 @@ date-fns "^1.27.2" figures "^1.7.0" -"@cypress/request@2.88.5": +"@cypress/request@^2.88.5": version "2.88.5" resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== @@ -1283,7 +1157,7 @@ tunnel-agent "^0.6.0" uuid "^3.3.2" -"@cypress/xvfb@1.2.4": +"@cypress/xvfb@^1.2.4": version "1.2.4" resolved "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== @@ -1339,7 +1213,7 @@ resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.1": +"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.1", "@emotion/is-prop-valid@^0.8.6": version "0.8.8" resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== @@ -1479,6 +1353,13 @@ unique-filename "^1.1.1" which "^1.3.1" +"@fmvilas/pseudo-yaml-ast@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@fmvilas/pseudo-yaml-ast/-/pseudo-yaml-ast-0.3.1.tgz#66c5df2c2d76ba8571dc5bd98fda4d7dce6121de" + integrity sha512-8OAB74W2a9M3k9bjYD8AjVXkX+qO8c0SqNT5HlgOqx7AxSw8xdksEcZp7gFtfi+4njSxT6+76ZR+1ubjAwQHOg== + dependencies: + yaml-ast-parser "0.0.43" + "@graphql-tools/delegate@6.0.15": version "6.0.15" resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.15.tgz#9e060bfc31fe7735bd5b2b401e98dea3fa5d3b25" @@ -1647,171 +1528,166 @@ resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/console/-/console-26.0.1.tgz#62b3b2fa8990f3cbffbef695c42ae9ddbc8f4b39" - integrity sha512-9t1KUe/93coV1rBSxMmBAOIK3/HVpwxArCA1CxskKyRiv6o8J70V8C/V3OJminVCTa2M0hQI9AWRd5wxu2dAHw== +"@jest/console@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/console/-/console-26.3.0.tgz#ed04063efb280c88ba87388b6f16427c0a85c856" + integrity sha512-/5Pn6sJev0nPUcAdpJHMVIsA8sKizL2ZkcKPE5+dJrCccks7tcM7c9wbgHudBJbxXLoTbqsHkG1Dofoem4F09w== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.0.1" - jest-util "^26.0.1" + jest-message-util "^26.3.0" + jest-util "^26.3.0" slash "^3.0.0" -"@jest/core@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/core/-/core-26.0.1.tgz#aa538d52497dfab56735efb00e506be83d841fae" - integrity sha512-Xq3eqYnxsG9SjDC+WLeIgf7/8KU6rddBxH+SCt18gEpOhAGYC/Mq+YbtlNcIdwjnnT+wDseXSbU0e5X84Y4jTQ== +"@jest/core@^26.4.2": + version "26.4.2" + resolved "https://registry.npmjs.org/@jest/core/-/core-26.4.2.tgz#85d0894f31ac29b5bab07aa86806d03dd3d33edc" + integrity sha512-sDva7YkeNprxJfepOctzS8cAk9TOekldh+5FhVuXS40+94SHbiicRO1VV2tSoRtgIo+POs/Cdyf8p76vPTd6dg== dependencies: - "@jest/console" "^26.0.1" - "@jest/reporters" "^26.0.1" - "@jest/test-result" "^26.0.1" - "@jest/transform" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/console" "^26.3.0" + "@jest/reporters" "^26.4.1" + "@jest/test-result" "^26.3.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^26.0.1" - jest-config "^26.0.1" - jest-haste-map "^26.0.1" - jest-message-util "^26.0.1" + jest-changed-files "^26.3.0" + jest-config "^26.4.2" + jest-haste-map "^26.3.0" + jest-message-util "^26.3.0" jest-regex-util "^26.0.0" - jest-resolve "^26.0.1" - jest-resolve-dependencies "^26.0.1" - jest-runner "^26.0.1" - jest-runtime "^26.0.1" - jest-snapshot "^26.0.1" - jest-util "^26.0.1" - jest-validate "^26.0.1" - jest-watcher "^26.0.1" + jest-resolve "^26.4.0" + jest-resolve-dependencies "^26.4.2" + jest-runner "^26.4.2" + jest-runtime "^26.4.2" + jest-snapshot "^26.4.2" + jest-util "^26.3.0" + jest-validate "^26.4.2" + jest-watcher "^26.3.0" micromatch "^4.0.2" p-each-series "^2.1.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/environment/-/environment-26.0.1.tgz#82f519bba71959be9b483675ee89de8c8f72a5c8" - integrity sha512-xBDxPe8/nx251u0VJ2dFAFz2H23Y98qdIaNwnMK6dFQr05jc+Ne/2np73lOAx+5mSBO/yuQldRrQOf6hP1h92g== +"@jest/environment@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-26.3.0.tgz#e6953ab711ae3e44754a025f838bde1a7fd236a0" + integrity sha512-EW+MFEo0DGHahf83RAaiqQx688qpXgl99wdb8Fy67ybyzHwR1a58LHcO376xQJHfmoXTu89M09dH3J509cx2AA== dependencies: - "@jest/fake-timers" "^26.0.1" - "@jest/types" "^26.0.1" - jest-mock "^26.0.1" + "@jest/fake-timers" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" + jest-mock "^26.3.0" -"@jest/fake-timers@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.0.1.tgz#f7aeff13b9f387e9d0cac9a8de3bba538d19d796" - integrity sha512-Oj/kCBnTKhm7CR+OJSjZty6N1bRDr9pgiYQr4wY221azLz5PHi08x/U+9+QpceAYOWheauLP8MhtSVFrqXQfhg== +"@jest/fake-timers@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.3.0.tgz#f515d4667a6770f60ae06ae050f4e001126c666a" + integrity sha512-ZL9ytUiRwVP8ujfRepffokBvD2KbxbqMhrXSBhSdAhISCw3gOkuntisiSFv+A6HN0n0fF4cxzICEKZENLmW+1A== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" "@sinonjs/fake-timers" "^6.0.1" - jest-message-util "^26.0.1" - jest-mock "^26.0.1" - jest-util "^26.0.1" + "@types/node" "*" + jest-message-util "^26.3.0" + jest-mock "^26.3.0" + jest-util "^26.3.0" -"@jest/globals@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/globals/-/globals-26.0.1.tgz#3f67b508a7ce62b6e6efc536f3d18ec9deb19a9c" - integrity sha512-iuucxOYB7BRCvT+TYBzUqUNuxFX1hqaR6G6IcGgEqkJ5x4htNKo1r7jk1ji9Zj8ZMiMw0oB5NaA7k5Tx6MVssA== +"@jest/globals@^26.4.2": + version "26.4.2" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-26.4.2.tgz#73c2a862ac691d998889a241beb3dc9cada40d4a" + integrity sha512-Ot5ouAlehhHLRhc+sDz2/9bmNv9p5ZWZ9LE1pXGGTCXBasmi5jnYjlgYcYt03FBwLmZXCZ7GrL29c33/XRQiow== dependencies: - "@jest/environment" "^26.0.1" - "@jest/types" "^26.0.1" - expect "^26.0.1" + "@jest/environment" "^26.3.0" + "@jest/types" "^26.3.0" + expect "^26.4.2" -"@jest/reporters@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-26.0.1.tgz#14ae00e7a93e498cec35b0c00ab21c375d9b078f" - integrity sha512-NWWy9KwRtE1iyG/m7huiFVF9YsYv/e+mbflKRV84WDoJfBqUrNRyDbL/vFxQcYLl8IRqI4P3MgPn386x76Gf2g== +"@jest/reporters@^26.4.1": + version "26.4.1" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-26.4.1.tgz#3b4d6faf28650f3965f8b97bc3d114077fb71795" + integrity sha512-aROTkCLU8++yiRGVxLsuDmZsQEKO6LprlrxtAuzvtpbIFl3eIjgIf3EUxDKgomkS25R9ZzwGEdB5weCcBZlrpQ== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.0.1" - "@jest/test-result" "^26.0.1" - "@jest/transform" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/console" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" graceful-fs "^4.2.4" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.0" + istanbul-lib-instrument "^4.0.3" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.0.1" - jest-resolve "^26.0.1" - jest-util "^26.0.1" - jest-worker "^26.0.0" + jest-haste-map "^26.3.0" + jest-resolve "^26.4.0" + jest-util "^26.3.0" + jest-worker "^26.3.0" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^4.1.3" + v8-to-istanbul "^5.0.1" optionalDependencies: - node-notifier "^7.0.0" + node-notifier "^8.0.0" -"@jest/source-map@^26.0.0": - version "26.0.0" - resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.0.0.tgz#fd7706484a7d3faf7792ae29783933bbf48a4749" - integrity sha512-S2Z+Aj/7KOSU2TfW0dyzBze7xr95bkm5YXNUqqCek+HE0VbNNSNzrRwfIi5lf7wvzDTSS0/ib8XQ1krFNyYgbQ== +"@jest/source-map@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.3.0.tgz#0e646e519883c14c551f7b5ae4ff5f1bfe4fc3d9" + integrity sha512-hWX5IHmMDWe1kyrKl7IhFwqOuAreIwHhbe44+XH2ZRHjrKIh0LO5eLQ/vxHFeAfRwJapmxuqlGAEYLadDq6ZGQ== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.0.1.tgz#1ffdc1ba4bc289919e54b9414b74c9c2f7b2b718" - integrity sha512-oKwHvOI73ICSYRPe8WwyYPTtiuOAkLSbY8/MfWF3qDEd/sa8EDyZzin3BaXTqufir/O/Gzea4E8Zl14XU4Mlyg== +"@jest/test-result@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.3.0.tgz#46cde01fa10c0aaeb7431bf71e4a20d885bc7fdb" + integrity sha512-a8rbLqzW/q7HWheFVMtghXV79Xk+GWwOK1FrtimpI5n1la2SY0qHri3/b0/1F0Ve0/yJmV8pEhxDfVwiUBGtgg== dependencies: - "@jest/console" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/console" "^26.3.0" + "@jest/types" "^26.3.0" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.0.1.tgz#b0563424728f3fe9e75d1442b9ae4c11da73f090" - integrity sha512-ssga8XlwfP8YjbDcmVhwNlrmblddMfgUeAkWIXts1V22equp2GMIHxm7cyeD5Q/B0ZgKPK/tngt45sH99yLLGg== +"@jest/test-sequencer@^26.4.2": + version "26.4.2" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.4.2.tgz#58a3760a61eec758a2ce6080201424580d97cbba" + integrity sha512-83DRD8N3M0tOhz9h0bn6Kl6dSp+US6DazuVF8J9m21WAp5x7CqSMaNycMP0aemC/SH/pDQQddbsfHRTBXVUgog== dependencies: - "@jest/test-result" "^26.0.1" + "@jest/test-result" "^26.3.0" graceful-fs "^4.2.4" - jest-haste-map "^26.0.1" - jest-runner "^26.0.1" - jest-runtime "^26.0.1" + jest-haste-map "^26.3.0" + jest-runner "^26.4.2" + jest-runtime "^26.4.2" -"@jest/transform@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/transform/-/transform-26.0.1.tgz#0e3ecbb34a11cd4b2080ed0a9c4856cf0ceb0639" - integrity sha512-pPRkVkAQ91drKGbzCfDOoHN838+FSbYaEAvBXvKuWeeRRUD8FjwXkqfUNUZL6Ke48aA/1cqq/Ni7kVMCoqagWA== +"@jest/transform@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-26.3.0.tgz#c393e0e01459da8a8bfc6d2a7c2ece1a13e8ba55" + integrity sha512-Isj6NB68QorGoFWvcOjlUhpkT56PqNIsXKR7XfvoDlCANn/IANlh8DrKAA2l2JKC3yWSMH5wS0GwuQM20w3b2A== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.0.1" + jest-haste-map "^26.3.0" jest-regex-util "^26.0.0" - jest-util "^26.0.1" + jest-util "^26.3.0" micromatch "^4.0.2" pirates "^4.0.1" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^24.9.0": - version "24.9.0" - resolved "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" - integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^13.0.0" - "@jest/types@^25.5.0": version "25.5.0" resolved "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" @@ -1822,16 +1698,51 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^26.0.1": - version "26.0.1" - resolved "https://registry.npmjs.org/@jest/types/-/types-26.0.1.tgz#b78333fbd113fa7aec8d39de24f88de8686dac67" - integrity sha512-IbtjvqI9+eS1qFnOIEL7ggWmT+iK/U+Vde9cGWtYb/b6XgKb3X44ZAe/z9YZzoAAZ/E92m0DqrilF934IGNnQA== +"@jest/types@^26.3.0": + version "26.3.0" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" + integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@kyleshockey/object-assign-deep@^0.4.2": + version "0.4.2" + resolved "https://registry.npmjs.org/@kyleshockey/object-assign-deep/-/object-assign-deep-0.4.2.tgz#84900f0eefc372798f4751b5262830b8208922ec" + integrity sha1-hJAPDu/DcnmPR1G1JigwuCCJIuw= + +"@kyleshockey/xml@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@kyleshockey/xml/-/xml-1.0.2.tgz#81fad3d7c33da2ba2639db095db3db24c2921f70" + integrity sha512-iMo32MPLcI9cPxs3YL5kmKxKgDmkSZDCFEqIT5eRk7d/Ll8r4X3SwGYSigzALd6+RHWlFEmjL1QyaQ15xDZFlw== + dependencies: + stream "^0.0.2" + +"@kyma-project/asyncapi-react@^0.11.0": + version "0.11.2" + resolved "https://registry.npmjs.org/@kyma-project/asyncapi-react/-/asyncapi-react-0.11.2.tgz#b523b0843da7c29a0d7569b3f31d7d8cbdf84b47" + integrity sha512-Wo6CgM3pzZaRMCVwXaEIrlXMuFtRNGli1GtFgMHQUB686daSCqSvRZUyitDHTZLofL3OI9JeYh5m6SjPji6bxg== + dependencies: + "@asyncapi/avro-schema-parser" "^0.1.2" + "@asyncapi/openapi-schema-parser" "^2.0.0" + "@asyncapi/parser" "^1.0.0-rc.1" + "@asyncapi/raml-dt-schema-parser" "^2.0.0" + constate "^1.2.0" + dompurify "^1.0.11" + json-schema-ref-parser "^9.0.6" + markdown-it "^9.1.0" + merge "^1.2.1" + openapi-sampler "^1.0.0-beta.15" + react-use "^12.2.0" + "@lerna/add@3.21.0": version "3.21.0" resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" @@ -2517,6 +2428,11 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@material-icons/font@^1.0.2": + version "1.0.3" + resolved "https://registry.npmjs.org/@material-icons/font/-/font-1.0.3.tgz#f722e5a69a03f20ef47d015cb69420bebeeaabe5" + integrity sha512-aIRd0Z9b/HJ/O24KOaP7dNsXypMnXhpWrpLVYvQB/JesVqXYSbioYND200sR+C14a0LSCp+qWnWCnSXmN1hWGw== + "@material-ui/core@^4.9.1": version "4.9.7" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.9.7.tgz#0c1caf123278770f34c5d8e9ecd9e1314f87a621" @@ -2675,15 +2591,6 @@ before-after-hook "^2.1.0" universal-user-agent "^5.0.0" -"@octokit/endpoint@^5.5.0": - version "5.5.3" - resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978" - integrity sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ== - dependencies: - "@octokit/types" "^2.0.0" - is-plain-object "^3.0.0" - universal-user-agent "^5.0.0" - "@octokit/endpoint@^6.0.1": version "6.0.3" resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487" @@ -2742,7 +2649,7 @@ "@octokit/types" "^5.0.0" deprecation "^2.3.1" -"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2": +"@octokit/request-error@^1.0.2": version "1.2.1" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== @@ -2760,21 +2667,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0": - version "5.3.2" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883" - integrity sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g== - dependencies: - "@octokit/endpoint" "^5.5.0" - "@octokit/request-error" "^1.0.1" - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - is-plain-object "^3.0.0" - node-fetch "^2.3.0" - once "^1.4.0" - universal-user-agent "^5.0.0" - -"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": +"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.0": version "5.4.5" resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b" integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg== @@ -2834,11 +2727,19 @@ dependencies: "@types/node" ">= 8" -"@open-draft/until@^1.0.0": +"@open-draft/until@^1.0.0", "@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== +"@openapi-contrib/openapi-schema-to-json-schema@^3.0.0": + version "3.0.3" + resolved "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.0.3.tgz#c626eab186938f2751ee54ec68b345133bc0065c" + integrity sha512-/WX/Jos8n7CxvtWPmhlKl9qCAAW0I+VR+V4yXfQxCmB8wmjiz6lPLTGjNk5zD15qi2MGv58++hQLLdow89KdkA== + dependencies: + fast-deep-equal "^3.1.3" + lodash.clonedeep "^4.5.0" + "@panva/asn1.js@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6" @@ -2897,10 +2798,10 @@ resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@reach/router@^1.2.1": - version "1.3.3" - resolved "https://registry.npmjs.org/@reach/router/-/router-1.3.3.tgz#58162860dce6c9449d49be86b0561b5ef46d80db" - integrity sha512-gOIAiFhWdiVGSVjukKeNKkCRBLmnORoTPyBihI/jLunICPgxdP30DroAvPQuf1eVfQbfGJQDJkwhJXsNPMnVWw== +"@reach/router@^1.2.1", "@reach/router@^1.3.3": + version "1.3.4" + resolved "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c" + integrity sha512-+mtn9wjlB9NN2CNnnC/BRYtwdKBfSyyasPYraNAyvaV1occr/5NnB4CVzjEZipNHwYebQwcndGUmpFzxAUoqSA== dependencies: create-react-context "0.3.0" invariant "^2.2.3" @@ -2925,9 +2826,52 @@ shortid "^2.2.14" "@rjsf/material-ui@^2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.1.0.tgz#a361b125af3a383b7f671634b8d254345f9f9311" - integrity sha512-9mMttnPNP6GiP7BtZGimYcYsbWwjyviqg/PD8oxrkEtZykULXOIdC3WDMJ3nPSym8RvZsgSnB8bajCpa5iSYQQ== + version "2.3.0" + resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.3.0.tgz#a051eb4db2ad778e39933a31ba806f415dd3ba90" + integrity sha512-v/xZ4Xk18ZgBARcCe99IDqdO97GU0UC784gG8PhGGFDdy5Rbdy7ixTjHkGYttkr43PB7SX6ttohNhkKSW4nATQ== + +"@roadiehq/backstage-plugin-github-pull-requests@0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.3.0.tgz#465cb74b1dab6f34a5a5bf4fa57b6c1d312bc5b7" + integrity sha512-nSyMh8p3SAYj1yj5/+Po82g7aZwF5pWb+fh7bLPdf+ywl5pa+aKQxAVNCvlvgfN2WZ9gDTMbCPyWDgCfeJ/V9g== + dependencies: + "@backstage/catalog-model" "^0.1.1-alpha.16" + "@backstage/core" "^0.1.1-alpha.16" + "@backstage/core-api" "^0.1.1-alpha.16" + "@backstage/plugin-catalog" "^0.1.1-alpha.16" + "@backstage/theme" "^0.1.1-alpha.16" + "@material-ui/core" "^4.9.1" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@octokit/rest" "^18.0.0" + "@octokit/types" "^5.0.1" + "@types/react-dom" "^16.9.8" + history "^5.0.0" + moment "^2.27.0" + react "^16.13.1" + react-dom "^16.13.1" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + +"@roadiehq/backstage-plugin-travis-ci@^0.1.4": + version "0.1.4" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-0.1.4.tgz#ddbb2ba5b9d47f474462ecbc8c769759578e8526" + integrity sha512-T1eJ0huhwZcRHjrDKWoAzlKBv8UbhXgrvE7wuHD7dCcmfQdln0jbn7cskOpTnVmrzGXWKQ6mDyKX3LfdEPYbAw== + dependencies: + "@backstage/core" "^0.1.1-alpha.18" + "@backstage/theme" "^0.1.1-alpha.18" + "@material-ui/core" "^4.9.1" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + date-fns "^2.15.0" + history "^5.0.0" + moment "^2.27.0" + react "^16.13.1" + react-dom "^16.13.1" + react-lazylog "^4.5.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" "@rollup/plugin-commonjs@^13.0.0": version "13.0.0" @@ -2992,6 +2936,21 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sindresorhus/is@^0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" + integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== + +"@sindresorhus/is@^2.0.0": + version "2.1.1" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1" + integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== + +"@sindresorhus/is@^3.1.1": + version "3.1.2" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz#548650de521b344e3781fbdb0ece4aa6f729afb8" + integrity sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ== + "@sinonjs/commons@^1.7.0": version "1.7.1" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1" @@ -3118,7 +3077,7 @@ regenerator-runtime "^0.13.3" util-deprecate "^1.0.2" -"@storybook/addons@5.3.19", "@storybook/addons@^5.3.17": +"@storybook/addons@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4" integrity sha512-Ky/k22p6i6FVNvs1VhuFyGvYJdcp+FgXqFgnPyY/OXJW/vPDapdElpTpHJZLFI9I2FQBDcygBPU5RXkumQ+KUQ== @@ -3131,6 +3090,21 @@ global "^4.3.2" util-deprecate "^1.0.2" +"@storybook/addons@^6.0.4": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.0.12.tgz#f57f89e0aa55febfb5437ddc2628a0ccc9f44f44" + integrity sha512-gVCyWK4jys5cUY0d3/Bxi02oeCsgdi6xVvA+T4v+SgeduAfm/k01tdO2qDXL37Sl+2TT9HBQGazDrsIUW4d7Ug== + dependencies: + "@storybook/api" "6.0.12" + "@storybook/channels" "6.0.12" + "@storybook/client-logger" "6.0.12" + "@storybook/core-events" "6.0.12" + "@storybook/router" "6.0.12" + "@storybook/theming" "6.0.12" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.3" + "@storybook/api@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.19.tgz#77f15e9e2eee59fe1ddeaba1ef39bc34713a6297" @@ -3157,6 +3131,32 @@ telejson "^3.2.0" util-deprecate "^1.0.2" +"@storybook/api@6.0.12": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.0.12.tgz#d6ded5c9ac8f989c4915e11a80a4db69341fc95f" + integrity sha512-8+jPtfhUVM1hT22OT4rjHRxkW924gbWrAxCFYUXOw80a0x7BcT4sL2ah1D4FWf0IpCT/onLf9jLvSVXr8V0xOw== + dependencies: + "@reach/router" "^1.3.3" + "@storybook/channels" "6.0.12" + "@storybook/client-logger" "6.0.12" + "@storybook/core-events" "6.0.12" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.0.12" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.0.12" + "@types/reach__router" "^1.3.5" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + react "^16.8.3" + regenerator-runtime "^0.13.3" + store2 "^2.7.1" + telejson "^5.0.2" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-5.3.19.tgz#ef9fe974c2a529d89ce342ff7acf5cc22805bae9" @@ -3175,6 +3175,15 @@ dependencies: core-js "^3.0.1" +"@storybook/channels@6.0.12": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.0.12.tgz#377f8049272f1862f9235a5051d95124d53fa08e" + integrity sha512-0EMtjde4tRrBnJj5jOXSgtMYfMxGZgoe/0hvVSJuOABf0FY5x6xrqNNDfory7+TtgieuoQE4idl2/tdHE6QJJA== + dependencies: + core-js "^3.0.1" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + "@storybook/client-api@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-5.3.19.tgz#7a5630bb8fffb92742b1773881e9004ee7fdf8e0" @@ -3205,6 +3214,14 @@ dependencies: core-js "^3.0.1" +"@storybook/client-logger@6.0.12": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.0.12.tgz#33b4c3cd1f1d98dab32d8c8c906301f1ab18f969" + integrity sha512-MEFDlBbbqcivF/Xmxitx/ky8kxN7TVBZ7K754/pPEI5q6UW32DecJIRg79UWp/1nBPMX/A0U3ORwv+0MjgDZBQ== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.19.tgz#aac1f9eea1247cc85bd93b10fca803876fb84a6b" @@ -3239,6 +3256,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.0.12": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.0.12.tgz#499ae06092103b149fcd9417d8e0baa356adc2c0" + integrity sha512-52yNnp+dBkHiG9S+rQO7Nv3PdSDi0XnBt7FoQ+v8H31vGpgdBLEhy8w5ZA4eTrL951VaU/4/XoOaG2+yPALaoA== + dependencies: + core-js "^3.0.1" + "@storybook/core@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/core/-/core-5.3.19.tgz#1e61f35c5148343a0c580f5d5efb77f3b4243a30" @@ -3379,6 +3403,26 @@ qs "^6.6.0" util-deprecate "^1.0.2" +"@storybook/router@6.0.12": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.0.12.tgz#f66d979ec01e15c11a378eb5dde3e696747ac184" + integrity sha512-vv1jHOOGelSzmDJnp9SdC/KR5RpE2am568ImOAQ9/XCmXNDhVshVlIS7ajy6yCKN/mS/63zKflbRNef+3SLU9Q== + dependencies: + "@reach/router" "^1.3.3" + "@types/reach__router" "^1.3.5" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + qs "^6.6.0" + +"@storybook/semver@^7.3.2": + version "7.3.2" + resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" + integrity sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg== + dependencies: + core-js "^3.6.5" + find-up "^4.1.0" + "@storybook/source-loader@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.19.tgz#ff0a00731c24c61721d8b9d84152f8542913a3b7" @@ -3413,6 +3457,24 @@ resolve-from "^5.0.0" ts-dedent "^1.1.0" +"@storybook/theming@6.0.12": + version "6.0.12" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.0.12.tgz#83b2099e7a7a5bd3acadb8e4106234ede62197c7" + integrity sha512-hmF6EIbm2A7G84+JR36UQWteElSwSNfGLzccAlUMiZIhdMG0SuCtyHe6FmckAWC226Mv+MW14fr+a4+OuRpM4g== + dependencies: + "@emotion/core" "^10.0.20" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.17" + "@storybook/client-logger" "6.0.12" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.4.4" + resolve-from "^5.0.0" + ts-dedent "^1.1.1" + "@storybook/ui@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-5.3.19.tgz#ac03b67320044a3892ee784111d4436b61874332" @@ -3699,17 +3761,7 @@ dependencies: "@babel/types" "^7.9.5" -"@svgr/plugin-jsx@4.3.x", "@svgr/plugin-jsx@^4.3.3": - version "4.3.3" - resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-4.3.3.tgz#e2ba913dbdfbe85252a34db101abc7ebd50992fa" - integrity sha512-cLOCSpNWQnDB1/v+SUENHH7a0XY09bfuMKdq9+gYvtuwzC2rU4I0wKGFEp1i24holdQdwodCtDQdFtJiTCWc+w== - dependencies: - "@babel/core" "^7.4.5" - "@svgr/babel-preset" "^4.3.3" - "@svgr/hast-util-to-babel-ast" "^4.3.2" - svg-parser "^2.0.0" - -"@svgr/plugin-jsx@^5.4.0": +"@svgr/plugin-jsx@5.4.x", "@svgr/plugin-jsx@^5.4.0": version "5.4.0" resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.4.0.tgz#ab47504c55615833c6db70fca2d7e489f509787c" integrity sha512-SGzO4JZQ2HvGRKDzRga9YFSqOqaNrgLlQVaGvpZ2Iht2gwRp/tq+18Pvv9kS9ZqOMYgyix2LLxZMY1LOe9NPqw== @@ -3719,6 +3771,16 @@ "@svgr/hast-util-to-babel-ast" "^5.4.0" svg-parser "^2.0.2" +"@svgr/plugin-jsx@^4.3.3": + version "4.3.3" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-4.3.3.tgz#e2ba913dbdfbe85252a34db101abc7ebd50992fa" + integrity sha512-cLOCSpNWQnDB1/v+SUENHH7a0XY09bfuMKdq9+gYvtuwzC2rU4I0wKGFEp1i24holdQdwodCtDQdFtJiTCWc+w== + dependencies: + "@babel/core" "^7.4.5" + "@svgr/babel-preset" "^4.3.3" + "@svgr/hast-util-to-babel-ast" "^4.3.2" + svg-parser "^2.0.0" + "@svgr/plugin-svgo@4.3.x", "@svgr/plugin-svgo@^4.3.1": version "4.3.1" resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-4.3.1.tgz#daac0a3d872e3f55935c6588dd370336865e9e32" @@ -3751,7 +3813,21 @@ "@svgr/plugin-svgo" "^5.4.0" rollup-pluginutils "^2.8.2" -"@svgr/webpack@4.3.x", "@svgr/webpack@^4.0.3": +"@svgr/webpack@5.4.x": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.4.0.tgz#b68bc86e29cf007292b96ced65f80971175632e0" + integrity sha512-LjepnS/BSAvelnOnnzr6Gg0GcpLmnZ9ThGFK5WJtm1xOqdBE/1IACZU7MMdVzjyUkfFqGz87eRE4hFaSLiUwYg== + dependencies: + "@babel/core" "^7.9.0" + "@babel/plugin-transform-react-constant-elements" "^7.9.0" + "@babel/preset-env" "^7.9.5" + "@babel/preset-react" "^7.9.4" + "@svgr/core" "^5.4.0" + "@svgr/plugin-jsx" "^5.4.0" + "@svgr/plugin-svgo" "^5.4.0" + loader-utils "^2.0.0" + +"@svgr/webpack@^4.0.3": version "4.3.3" resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-4.3.3.tgz#13cc2423bf3dff2d494f16b17eb7eacb86895017" integrity sha512-bjnWolZ6KVsHhgyCoYRFmbd26p8XVbulCzSG53BDQqAr+JOAderYK7CuYrB3bDjHJuF6LJ7Wrr42+goLRV9qIg== @@ -3772,24 +3848,32 @@ dependencies: defer-to-connect "^1.0.1" -"@testing-library/cypress@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@testing-library/cypress/-/cypress-6.0.0.tgz#935f7716e0e495f02fd753a42621e4d350097dce" - integrity sha512-vWPQtPsIDk5STOH2XdJbJoYq9gxOSAItP0ail+MlylK230zNkf3ODKd6eqWnDdruuqrhTF3CyqvPNMA8Xks/UQ== +"@szmarczak/http-timer@^4.0.0", "@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== dependencies: - "@babel/runtime" "^7.8.7" - "@testing-library/dom" "^7.0.2" - "@types/testing-library__cypress" "^5.0.3" + defer-to-connect "^2.0.0" -"@testing-library/dom@^7.0.2", "@testing-library/dom@^7.17.1": - version "7.17.2" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.17.2.tgz#d062e41336b885107bca8ffc7022eaf37cccaee1" - integrity sha512-TQAoIzoI9g64oNVJ+13i4cCh/DQp/n7fJOyMqlFB1oQVusPtSgiPImyb52CwtvPO7J0Im0+/4YcmxU9a+cVxxw== +"@testing-library/cypress@^6.0.0": + version "6.0.1" + resolved "https://registry.npmjs.org/@testing-library/cypress/-/cypress-6.0.1.tgz#c924a69617db6a403c498abcb63759b79318fc76" + integrity sha512-hcPu2OnVuSTX1yDubEe7TBRD6mP2VgyopP1WTBIrJP79INPZdgOAX+TMNH68uZ/r5b4C7100IB4hjPIEQ+/Xog== + dependencies: + "@babel/runtime" "^7.11.2" + "@testing-library/dom" "^7.22.2" + "@types/testing-library__cypress" "^5.0.6" + +"@testing-library/dom@^7.11.0", "@testing-library/dom@^7.17.1", "@testing-library/dom@^7.22.2": + version "7.23.0" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.23.0.tgz#c54c0fa53705ad867bcefb52fc0c96487fbc10f6" + integrity sha512-H5m090auYH+obdZmsaYLrSWC5OauWD2CvNbz88KBxQJoXgkJzbU0DpAG8BS7Evj5WqCC3nAAKrLS6vw0ljUYLg== dependencies: "@babel/runtime" "^7.10.3" + "@types/aria-query" "^4.2.0" aria-query "^4.2.2" - dom-accessibility-api "^0.4.5" - pretty-format "^25.5.0" + dom-accessibility-api "^0.5.1" + pretty-format "^26.4.2" "@testing-library/jest-dom@^5.10.1": version "5.10.1" @@ -3896,6 +3980,22 @@ resolved "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== +"@types/aria-query@^4.2.0": + version "4.2.0" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" + integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== + +"@types/babel__core@^7.0.0": + version "7.1.9" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" + integrity sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__core@^7.1.7": version "7.1.7" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -3937,6 +4037,23 @@ "@types/connect" "*" "@types/node" "*" +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + +"@types/cheerio@^0.22.8": + version "0.22.21" + resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" + integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q== + dependencies: + "@types/node" "*" + "@types/classnames@^2.2.9": version "2.2.10" resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999" @@ -3949,10 +4066,10 @@ dependencies: "@types/node" "*" -"@types/codemirror@^0.0.96": - version "0.0.96" - resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.96.tgz#73b52e784a246cebef31d544fef45ea764de5bad" - integrity sha512-GTswEV26Bl1byRxpD3sKd1rT2AISr0rK9ImlJgEzfvqhcVWeu4xQKFQI6UgSC95NT5swNG4st/oRMeGVZgPj9w== +"@types/codemirror@^0.0.97": + version "0.0.97" + resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.97.tgz#6f2d8266b7f1b34aacfe8c77221fafe324c3d081" + integrity sha512-n5d7o9nWhC49DjfhsxANP7naWSeTzrjXASkUDQh7626sM4zK9XP2EVcHp1IcCf/IPV6c7ORzDUDF3Bkt231VKg== dependencies: "@types/tern" "*" @@ -4014,6 +4131,11 @@ resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== +"@types/cookie@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" + integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== + "@types/cookiejar@*": version "2.1.1" resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" @@ -4053,14 +4175,7 @@ resolved "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ== -"@types/dockerode@^2.5.32": - version "2.5.32" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.32.tgz#52d3628f605f8ea65202541c59a8a6dd166384fd" - integrity sha512-TfaGOoOHxsjkWRj2sPoQ3FLmTC5mVMhZ4kzZy13U7mjtIDoloE4e7AMj5jPLbffWB6Csy5DF5e0lC9M+tnKz/A== - dependencies: - "@types/node" "*" - -"@types/dockerode@^2.5.34": +"@types/dockerode@^2.5.32", "@types/dockerode@^2.5.34": version "2.5.34" resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.34.tgz#9adb884f7cc6c012a6eb4b2ad794cc5d01439959" integrity sha512-LcbLGcvcBwBAvjH9UrUI+4qotY+A5WCer5r43DR5XHv2ZIEByNXFdPLo1XxR+v/BjkGjlggW8qUiXuVEhqfkpA== @@ -4104,7 +4219,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.17.6": +"@types/express@*", "@types/express@4.17.7", "@types/express@^4.17.6": version "4.17.7" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59" integrity sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ== @@ -4114,16 +4229,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@4.17.4": - version "4.17.4" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.4.tgz#e78bf09f3f530889575f4da8a94cd45384520aac" - integrity sha512-DO1L53rGqIDUEvOjJKmbMEQ5Z+BM2cIEPy/eV3En+s166Gz+FeuzRerxcab757u/U4v4XF4RYrZPmqKa+aY/2w== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "*" - "@types/qs" "*" - "@types/serve-static" "*" - "@types/fs-capacitor@*": version "2.0.0" resolved "https://registry.npmjs.org/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e" @@ -4158,9 +4263,9 @@ "@types/node" "*" "@types/google-protobuf@^3.7.2": - version "3.7.2" - resolved "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.7.2.tgz#cd8a360c193ce4d672575a20a79f49ba036d38d2" - integrity sha512-ifFemzjNchFBCtHS6bZNhSZCBu7tbtOe0e8qY0z2J4HtFXmPJjm6fXSaQsTG7yhShBEZtt2oP/bkwu5k+emlkQ== + version "3.7.3" + resolved "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.7.3.tgz#429512e541bbd777f2c867692e6335ee08d1f6d4" + integrity sha512-FRwj40euE2bYkG+0X5w2nEA8yAzgJRcEa7RBd0Gsdkb9/tPM2pctVVAvnOUTbcXo2VmIHPo0Ae94Gl9vRHfKzg== "@types/graceful-fs@^4.1.2": version "4.1.3" @@ -4179,10 +4284,10 @@ "@types/koa" "*" graphql "^14.5.3" -"@types/helmet@^0.0.47": - version "0.0.47" - resolved "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.47.tgz#ec5161541b649142205b7c558bca14801c5ce129" - integrity sha512-TcHA/djjdUtrMtq/QAayVLrsgjNNZ1Uhtz0KhfH01mrmjH44E54DA1A0HNbwW0H/NBFqV+tGMo85ACuEhMXcdg== +"@types/helmet@^0.0.48": + version "0.0.48" + resolved "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.48.tgz#e754399d2f4672ba63962e8490efd3edd31d9799" + integrity sha512-C7MpnvSDrunS1q2Oy1VWCY7CDWHozqSnM8P4tFeRTuzwqni+PYOjEredwcqWG+kLpYcgLsgcY3orHB54gbx2Jw== dependencies: "@types/express" "*" @@ -4219,6 +4324,11 @@ resolved "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ== +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + "@types/http-errors@^1.6.3": version "1.8.0" resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz#682477dbbbd07cd032731cb3b0e7eaee3d026b69" @@ -4240,10 +4350,10 @@ dependencies: "@types/node" "*" -"@types/inquirer@^6.5.0": - version "6.5.0" - resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" - integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== +"@types/inquirer@^7.3.1": + version "7.3.1" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" + integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g== dependencies: "@types/through" "*" rxjs "^6.4.0" @@ -4273,10 +4383,17 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest@*", "@types/jest@^26.0.7": - version "26.0.7" - resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.7.tgz#495cb1d1818c1699dbc3b8b046baf1c86ef5e324" - integrity sha512-+x0077/LoN6MjqBcVOe1y9dpryWnfDZ+Xfo3EqGeBcfPRJlQp3Lw62RvNlWxuGv7kOEwlHriAa54updi3Jvvwg== + version "26.0.9" + resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.9.tgz#0543b57da5f0cd949c5f423a00c56c492289c989" + integrity sha512-k4qFfJ5AUKrWok5KYXp2EPm89b0P/KZpl7Vg4XuOTVVQEhLDBDBU3iBFrjjdgd8fLw96aAtmnwhXHl63bWeBQQ== dependencies: jest-diff "^25.2.1" pretty-format "^25.2.1" @@ -4294,9 +4411,9 @@ integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5": - version "7.0.5" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + version "7.0.6" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/json5@^0.0.29": version "0.0.29" @@ -4313,6 +4430,13 @@ resolved "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== +"@types/keyv@*", "@types/keyv@^3.1.1": + version "3.1.1" + resolved "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + "@types/koa-compose@*": version "3.2.5" resolved "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" @@ -4334,9 +4458,9 @@ "@types/node" "*" "@types/lodash@^4.14.151": - version "4.14.155" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" - integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== + version "4.14.161" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" + integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== "@types/long@^4.0.0": version "4.0.1" @@ -4402,10 +4526,10 @@ resolved "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== -"@types/nodegit@0.26.7": - version "0.26.7" - resolved "https://registry.npmjs.org/@types/nodegit/-/nodegit-0.26.7.tgz#3fc8f5108dcabddd6238509748c0e812ba02b9b7" - integrity sha512-qVwQupq4To/wkRtUnaiwbNhuRiVJShOG9CYIWiGdKQRkvWijtHmEnrC1KJHOVrzK04sMdHRClyecrfmN17VMFQ== +"@types/nodegit@0.26.8": + version "0.26.8" + resolved "https://registry.npmjs.org/@types/nodegit/-/nodegit-0.26.8.tgz#81a79307fbdd1cf028a8ec7cbc94020bac54be6f" + integrity sha512-CwcynPazar6cJxjn8PG2RIujNgFvH2pAoA/bBBvPTUrONVzc9hj0DRMYzioy7zI/+e8TfJJLP+DK26mEU0eErg== dependencies: "@types/node" "*" @@ -4456,6 +4580,13 @@ "@types/passport" "*" "@types/passport-oauth2" "*" +"@types/passport-microsoft@^0.0.0": + version "0.0.0" + resolved "https://registry.npmjs.org/@types/passport-microsoft/-/passport-microsoft-0.0.0.tgz#ba71bccdd793711239d6b02e8d5953c21abc1c8d" + integrity sha512-jfkltRosn+P/+RoFMTl+mCyBTgPTFhjDEF832j7fmlYpuf+5yuzPLz7Rm5XMKN/Gqpro6myCyGPTuCc4yBQ2jQ== + dependencies: + "@types/passport-oauth2" "*" + "@types/passport-oauth2@*": version "1.4.9" resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92" @@ -4505,10 +4636,10 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/reach__router@^1.2.3": - version "1.3.1" - resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.1.tgz#ca8b431acb12bb897d2b806f6fdd815f056d6d02" - integrity sha512-E51ntVeunnxofXmOoPFiOvElHWf+jBEs3B56gGx7XhPHOkJdjWxWDY4V1AsUiwhtOCXPM7atFy30wj7glyv2Fg== +"@types/reach__router@^1.2.3", "@types/reach__router@^1.3.5": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.5.tgz#14e1e981cccd3a5e50dc9e969a72de0b9d472f6d" + integrity sha512-h0NbqXN/tJuBY/xggZSej1SKQEstbHO7J/omt1tYoFGmj3YXOodZKbbqD4mNDh7zvEGYd7YFrac1LTtAr3xsYQ== dependencies: "@types/history" "*" "@types/react" "*" @@ -4531,10 +4662,10 @@ dependencies: "@types/react" "*" -"@types/react-helmet@^5.0.15": - version "5.0.15" - resolved "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-5.0.15.tgz#af0370617307e9f062c6aee0f1f5588224ce646e" - integrity sha512-CCjqvecDJTXRrHG8aTc2YECcQCl26za/q+NaBRvy/wtm0Uh38koM2dpv2bG1xJV4ckz3t1lm2/5KU6nt2s9BWg== +"@types/react-helmet@^6.1.0": + version "6.1.0" + resolved "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.0.tgz#af586ed685f4905e2adc7462d1d65ace52beee7a" + integrity sha512-PYRoU1XJFOzQ3BHvWL1T8iDNbRjdMDJMT5hFmZKGbsq09kbSqJy61uwEpTrbTNWDopVphUT34zUSVLK9pjsgYQ== dependencies: "@types/react" "*" @@ -4581,6 +4712,13 @@ dependencies: "@types/react" "*" +"@types/react-wait@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@types/react-wait/-/react-wait-0.3.1.tgz#193cbd8fe86baa53b6f65dfa73f03d562f462a27" + integrity sha512-BS9AEjWZItDgpx6LlICcuf53M27zBFCsHx/llCbrmrt/WI7ecG2LGquCss3n8O8bwEDiTX4yYLjy8yLeIfgYTg== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.9": version "16.9.37" resolved "https://registry.npmjs.org/@types/react/-/react-16.9.37.tgz#8fb93e7dbd5b1d3796f69aa979a7fe0439bc7bea" @@ -4589,6 +4727,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/react@16.4.6": + version "16.4.6" + resolved "https://registry.npmjs.org/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada" + integrity sha512-9LDZdhsuKSc+DjY65SjBkA958oBWcTWSVWAd2cD9XqKBjhGw1KzAkRhWRw2eIsXvaIE/TOTjjKMFVC+JA1iU4g== + dependencies: + csstype "^2.2.0" + "@types/recursive-readdir@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.0.tgz#b39cd5474fd58ea727fe434d5c68b7a20ba9121c" @@ -4608,6 +4753,13 @@ dependencies: "@types/node" "*" +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/rollup-plugin-peer-deps-external@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@types/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.0.tgz#eae7d8b9d27fa037f5bcaded24e389f85b81973c" @@ -4617,9 +4769,9 @@ rollup "^0.63.4" "@types/rollup-plugin-postcss@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/rollup-plugin-postcss/-/rollup-plugin-postcss-2.0.0.tgz#b1bcc686c7dbe441f2fcfb47cac01acc508f46c0" - integrity sha512-lw4LvCcWpQ/Yomb9bxT69uAN5ZfW6nhL9itEu3z0IzsNNqKalwIDo7pEK8jxoiHQmcPaYzayAfo4M1JT/h8crw== + version "2.0.1" + resolved "https://registry.npmjs.org/@types/rollup-plugin-postcss/-/rollup-plugin-postcss-2.0.1.tgz#f2c73a458b035db8af361c4a7d921f74c6d4dffa" + integrity sha512-q+o1V6536wwIIlwJzEjm09emg/1QIceJXo01h6fTA1JJY4p/QQTOc10mizp4NhmwRMB8LVOThCgkwvQgzvJFjQ== dependencies: "@types/cssnano" "*" "@types/node" "*" @@ -4640,12 +4792,12 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/sinonjs__fake-timers@6.0.1": +"@types/sinonjs__fake-timers@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== -"@types/sizzle@*", "@types/sizzle@2.3.2": +"@types/sizzle@*", "@types/sizzle@^2.3.2": version "2.3.2" resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -4696,6 +4848,13 @@ dependencies: "@types/superagent" "*" +"@types/swagger-ui-react@^3.23.3": + version "3.23.3" + resolved "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-3.23.3.tgz#56bcf3872d8655500e6d51d29ba3916397806fa3" + integrity sha512-JKWYWAliabBxMxURagpRR6RHLEZxxp2W2tf1IN7ftqye6dIxC2e4mh4eZzARPG8+2OpzKlodHIOIwueFTYWaag== + dependencies: + "@types/react" "*" + "@types/tapable@*", "@types/tapable@^1.0.5": version "1.0.5" resolved "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" @@ -4716,21 +4875,14 @@ dependencies: "@types/estree" "*" -"@types/testing-library__cypress@^5.0.3": - version "5.0.3" - resolved "https://registry.npmjs.org/@types/testing-library__cypress/-/testing-library__cypress-5.0.3.tgz#94969b7c1eea96e09d8e023a1d225590fa75a1fe" - integrity sha512-efMwaVnsWEBDz0Ikm84Kh/oiUxMfz5LQtWx/Y6s2Bj0KkQ6+569/101X3Gz2bJy+otaXWPzOUOwDeAfzOJWoeQ== +"@types/testing-library__cypress@^5.0.6": + version "5.0.6" + resolved "https://registry.npmjs.org/@types/testing-library__cypress/-/testing-library__cypress-5.0.6.tgz#9015f575c1a98f05996a4fe769071134ee488c26" + integrity sha512-TUp5wfanU7zUZigKqIeQDChnHQ1MEzbYqrI5iCQMFiesWNOASWm/el1lFBh1JPqmd6GkdDdDiHYJnkqd9le2ww== dependencies: - "@types/testing-library__dom" "*" + "@testing-library/dom" "^7.11.0" cypress "*" -"@types/testing-library__dom@*": - version "6.14.0" - resolved "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz#1aede831cb4ed4a398448df5a2c54b54a365644e" - integrity sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA== - dependencies: - pretty-format "^24.3.0" - "@types/testing-library__jest-dom@^5.9.1": version "5.9.1" resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.1.tgz#aba5ee062b7880f69c212ef769389f30752806e5" @@ -4780,10 +4932,10 @@ resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== -"@types/webpack-node-externals@^1.7.1": - version "1.7.1" - resolved "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-1.7.1.tgz#73d0d7ae0e98cfbd69b7443388302cd69217494a" - integrity sha512-kbO2gYPKvMb5j1KOgnIuUH52CKul9Ud4b10J5n+JX8oHmgu86hYpBVfrV4bMDe5lhCaO64h8QrKz7WnRZzqkbA== +"@types/webpack-node-externals@^2.5.0": + version "2.5.0" + resolved "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-2.5.0.tgz#bcd161af84a4960416e5850e06931b35321c6654" + integrity sha512-KaWfhUQlpWknM/CMBKhV7i0vxX/N2xEy3WeaE500s4ZNxC4nLnKB+0F3gD3Fg+5octPq0nn8ZlfFR/P3dSkXpw== dependencies: "@types/webpack" "*" @@ -4797,9 +4949,9 @@ source-map "^0.6.1" "@types/webpack@*", "@types/webpack@^4.41.7", "@types/webpack@^4.41.8": - version "4.41.17" - resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz#0a69005e644d657c85b7d6ec1c826a71bebd1c93" - integrity sha512-6FfeCidTSHozwKI67gIVQQ5Mp0g4X96c2IXxX75hYEQJwST/i6NyZexP//zzMOBb+wG9jJ7oO8fk9yObP2HWAw== + version "4.41.21" + resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz#cc685b332c33f153bb2f5fc1fa3ac8adeb592dee" + integrity sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA== dependencies: "@types/anymatch" "*" "@types/node" "*" @@ -4839,13 +4991,6 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== -"@types/yargs@^13.0.0": - version "13.0.8" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz#a38c22def2f1c2068f8971acb3ea734eb3c64a99" - integrity sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^15.0.0": version "15.0.4" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" @@ -5166,12 +5311,7 @@ acorn@^6.0.1, acorn@^6.4.1: resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" - integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== - -acorn@^7.2.0: +acorn@^7.1.1, acorn@^7.2.0: version "7.3.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== @@ -5243,10 +5383,30 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0: - version "6.12.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" - integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== +ajv@6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz#678495f9b82f7cca6be248dd92f59bff5e1f4360" + integrity sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.1" + +ajv@^5.0.0: + version "5.5.2" + resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0: + version "6.12.3" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -5287,6 +5447,13 @@ ansi-html@0.0.7: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= +ansi-red@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" + integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= + dependencies: + ansi-wrap "0.1.0" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -5297,7 +5464,7 @@ ansi-regex@^3.0.0: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0, ansi-regex@^4.1.0: +ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== @@ -5334,6 +5501,11 @@ ansi-to-html@^0.6.11: dependencies: entities "^1.1.2" +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + any-observable@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -5433,10 +5605,10 @@ apollo-server-caching@^0.5.2: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.16.0: - version "2.16.0" - resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.16.0.tgz#56b367db49f97b7da03b29cef89b63d9ed14ee0c" - integrity sha512-mnvg2cPvsQtjFXIqIhEAbPqGyiSXDSbiBgNQ8rY8g7r2eRMhHKZePqGF03gP1/w87yVaSDRAZBDk6o+jiBXjVQ== +apollo-server-core@^2.17.0: + version "2.17.0" + resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.17.0.tgz#6af697ffe4968e74add01cd1efd2a8fb33299cf3" + integrity sha512-rjAkBbKSrGLDfg/g5bohnPlQahmkAxgEBuMDVsoF3aa+RaEPXPUMYrLbOxntl0LWeLbPiMa/IyFF43dvlGqV7w== dependencies: "@apollographql/apollo-tools" "^0.4.3" "@apollographql/graphql-playground-html" "1.6.26" @@ -5450,7 +5622,7 @@ apollo-server-core@^2.16.0: apollo-server-errors "^2.4.2" apollo-server-plugin-base "^0.9.1" apollo-server-types "^0.5.1" - apollo-tracing "^0.11.1" + apollo-tracing "^0.11.2" fast-json-stable-stringify "^2.0.0" graphql-extensions "^0.12.4" graphql-tag "^2.9.2" @@ -5474,18 +5646,18 @@ apollo-server-errors@^2.4.2: resolved "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz#1128738a1d14da989f58420896d70524784eabe5" integrity sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ== -apollo-server-express@^2.16.0: - version "2.16.0" - resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.16.0.tgz#3474a7f7eb868a2a847a364839147f8d7f26454a" - integrity sha512-mBIvKcF8gApj7wbmqe0A4Tsy+Pw66mI6cmtD912bG59KhUBveSCZ21dDlRSvnXUyK+GOo2ItwcUEtmks+Z2Pqw== +apollo-server-express@^2.16.0, apollo-server-express@^2.17.0: + version "2.17.0" + resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.17.0.tgz#2014559b75a0bcf7ff8cf0f2d077da6653abbc18" + integrity sha512-PonpWOuM1DH3Cz0bu56Tusr3GXOnectC6AD/gy2GXK0v84E7tKTuxEY3SgsgxhvfvvhfwJbXTyIogL/wezqnCw== dependencies: "@apollographql/graphql-playground-html" "1.6.26" "@types/accepts" "^1.3.5" "@types/body-parser" "1.19.0" "@types/cors" "^2.8.4" - "@types/express" "4.17.4" + "@types/express" "4.17.7" accepts "^1.3.5" - apollo-server-core "^2.16.0" + apollo-server-core "^2.17.0" apollo-server-types "^0.5.1" body-parser "^1.18.3" cors "^2.8.4" @@ -5513,20 +5685,20 @@ apollo-server-types@^0.5.1: apollo-server-env "^2.4.5" apollo-server@^2.16.0: - version "2.16.0" - resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.16.0.tgz#fa30e29b78e8cb70b2c81d0f7b96953beb3d4baf" - integrity sha512-zbEe0FSqatqE6bmIfq/o1/Hsqc596ZOwZM/L8Ttwa4ucQ1ybqf1ZejSYu6ehFEj1G6rOBY1ttVKkIllcErK4GQ== + version "2.17.0" + resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.17.0.tgz#d015fe6d8d6bdd468bc43ecabcb29ceb41e0b286" + integrity sha512-vVMu+VqjmsB6yk5iNTb/AXM6EJGd2uwzrcTAbwqvGI7GqCYDRZlGBwrQCjOU/jT/EPWdNRWks/qhJYiQMeVXSg== dependencies: - apollo-server-core "^2.16.0" - apollo-server-express "^2.16.0" + apollo-server-core "^2.17.0" + apollo-server-express "^2.17.0" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@^0.11.1: - version "0.11.1" - resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.11.1.tgz#3e3a4ce4b21e57dcc57b10bbd539db243b752606" - integrity sha512-l7g+uILw7v32GA46IRXIx5XXbZhFI96BhSqrGK9yyvfq+NMcvVZrj3kIhRImPGhAjMdV+5biA/jztabElAbDjg== +apollo-tracing@^0.11.2: + version "0.11.2" + resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.11.2.tgz#14308b176e021f5e6ec3ee670f8f96e9fbfdb50c" + integrity sha512-QjmRd2ozGD+PfmF6U9w/w6jrclYSBNczN6Bzppr8qA5somEGl5pqdprIZYL28H0IapZiutA3x6p6ZVF/cVX8wA== dependencies: apollo-server-env "^2.4.5" apollo-server-plugin-base "^0.9.1" @@ -5556,11 +5728,18 @@ aproba@^2.0.0: resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -arch@2.1.2: +arch@^2.1.0, arch@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== +archive-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz#f92e72233056dfc6969472749c267bdb046b1d70" + integrity sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA= + dependencies: + file-type "^4.2.0" + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -5574,7 +5753,7 @@ arg@^4.1.0: resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^1.0.7: +argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -5828,18 +6007,32 @@ atob@^2.1.2: resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.7.2: - version "9.7.4" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378" - integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g== +autolinker@^3.11.0: + version "3.14.1" + resolved "https://registry.npmjs.org/autolinker/-/autolinker-3.14.1.tgz#6ae4b812b6eaf42d4d68138b9e67757cbf2bc1e4" + integrity sha512-yvsRHIaY51EYDml6MGlbqyJGfl4n7zezGYf+R7gvM8c5LNpRGc4SISkvgAswSS8SWxk/OrGCylKV9mJyVstz7w== dependencies: - browserslist "^4.8.3" - caniuse-lite "^1.0.30001020" - chalk "^2.4.2" + tslib "^1.9.3" + +autolinker@~0.28.0: + version "0.28.1" + resolved "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47" + integrity sha1-BlK0kYgYefB3XazgzcoyM5QqTkc= + dependencies: + gulp-header "^1.7.1" + +autoprefixer@^9.7.2, autoprefixer@^9.7.5: + version "9.8.6" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.26" - postcss-value-parser "^4.0.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" aws-sign2@~0.7.0: version "0.7.0" @@ -5858,6 +6051,13 @@ axios@^0.19.0, axios@^0.19.2: dependencies: follow-redirects "1.5.10" +axios@^0.20.0: + version "0.20.0" + resolved "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== + dependencies: + follow-redirects "^1.10.0" + axobject-query@^2.0.2: version "2.1.2" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz#2bdffc0371e643e5f03ba99065d5179b9ca79799" @@ -5907,16 +6107,16 @@ babel-helper-to-multiple-sequence-expressions@^0.5.0: resolved "https://registry.npmjs.org/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d" integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA== -babel-jest@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.0.1.tgz#450139ce4b6c17174b136425bda91885c397bc46" - integrity sha512-Z4GGmSNQ8pX3WS1O+6v3fo41YItJJZsVxG5gIQ+HuB/iuAQBJxMTHTwz292vuYws1LnHfwSRgoqI+nxdy/pcvw== +babel-jest@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.3.0.tgz#10d0ca4b529ca3e7d1417855ef7d7bd6fc0c3463" + integrity sha512-sxPnQGEyHAOPF8NcUsD0g7hDCnvLL2XyblRBcgrzTWBB/mAIpWow3n1bEL+VghnnZfreLhFSBsFluRoK2tRK4g== dependencies: - "@jest/transform" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.0.0" + babel-preset-jest "^26.3.0" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" @@ -5960,13 +6160,14 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.0.0.tgz#fd1d35f95cf8849fc65cb01b5e58aedd710b34a8" - integrity sha512-+AuoehOrjt9irZL7DOt2+4ZaTM6dlu1s5TTS46JBa0/qem4dy7VNW3tMb96qeEqcIh20LD73TVNtmVEeymTG7w== +babel-plugin-jest-hoist@^26.2.0: + version "26.2.0" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.2.0.tgz#bdd0011df0d3d513e5e95f76bd53b51147aca2dd" + integrity sha512-B/hVMRv8Nh1sQ1a3EY8I0n4Y1Wty3NrR5ebOyVT302op+DOAau+xNEImGMsUWOC3++ZlMooCytKz+NgN8aKGbA== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.7.0: @@ -6132,14 +6333,15 @@ babel-plugin-transform-undefined-to-void@^6.9.4: resolved "https://registry.npmjs.org/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280" integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA= -babel-preset-current-node-syntax@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz#fb4a4c51fe38ca60fede1dc74ab35eb843cb41d6" - integrity sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw== +babel-preset-current-node-syntax@^0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da" + integrity sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" @@ -6148,13 +6350,13 @@ babel-preset-current-node-syntax@^0.1.2: "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -babel-preset-jest@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.0.0.tgz#1eac82f513ad36c4db2e9263d7c485c825b1faa6" - integrity sha512-9ce+DatAa31DpR4Uir8g4Ahxs5K4W4L8refzt+qHWQANb6LhGcAEfIFgLUwk67oya2cCUd6t4eUMtO/z64ocNw== +babel-preset-jest@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.3.0.tgz#ed6344506225c065fd8a0b53e191986f74890776" + integrity sha512-5WPdf7nyYi2/eRxCbVrE1kKCWxgWY4RsPEbdJWFm7QsesFGqjdkyLeu1zRkwM1cxK6EPIlNd6d2AxLk7J+t4pw== dependencies: - babel-plugin-jest-hoist "^26.0.0" - babel-preset-current-node-syntax "^0.1.2" + babel-plugin-jest-hoist "^26.2.0" + babel-preset-current-node-syntax "^0.1.3" "babel-preset-minify@^0.5.0 || 0.6.0-alpha.5": version "0.5.1" @@ -6193,6 +6395,11 @@ babel-runtime@6.26.0, babel-runtime@^6.26.0: core-js "^2.4.0" regenerator-runtime "^0.11.0" +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + backo2@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -6213,7 +6420,7 @@ base64-arraybuffer@^0.1.5: resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= -base64-js@^1.0.2: +base64-js@^1.0.2, base64-js@^1.2.0: version "1.3.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== @@ -6280,6 +6487,54 @@ big.js@^5.2.2: resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bin-build@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz#c5780a25a8a9f966d8244217e6c1f5082a143861" + integrity sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA== + dependencies: + decompress "^4.0.0" + download "^6.2.2" + execa "^0.7.0" + p-map-series "^1.0.0" + tempfile "^2.0.0" + +bin-check@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz#fc495970bdc88bb1d5a35fc17e65c4a149fc4a49" + integrity sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA== + dependencies: + execa "^0.7.0" + executable "^4.1.0" + +bin-version-check@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz#7d819c62496991f80d893e6e02a3032361608f71" + integrity sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ== + dependencies: + bin-version "^3.0.0" + semver "^5.6.0" + semver-truncate "^1.1.2" + +bin-version@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz#5b09eb280752b1bd28f0c9db3f96f2f43b6c0839" + integrity sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ== + dependencies: + execa "^1.0.0" + find-versions "^3.0.0" + +bin-wrapper@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz#99348f2cf85031e3ef7efce7e5300aeaae960605" + integrity sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q== + dependencies: + bin-check "^4.1.0" + bin-version-check "^4.0.0" + download "^7.1.0" + import-lazy "^3.1.0" + os-filter-obj "^2.0.0" + pify "^4.0.1" + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -6297,6 +6552,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + bl@^1.0.0: version "1.2.2" resolved "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -6314,7 +6574,7 @@ bl@^4.0.1: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -6324,7 +6584,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0: +body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -6340,6 +6600,16 @@ body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0: raw-body "2.4.0" type-is "~1.6.17" +body@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069" + integrity sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk= + dependencies: + continuable-cache "^0.3.1" + error "^7.0.0" + raw-body "~1.1.0" + safe-json-parse "~1.0.1" + bonjour@^3.5.0: version "3.5.0" resolved "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" @@ -6476,7 +6746,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.10.0, browserslist@^4.0.0, browserslist@^4.8.3: +browserslist@4.10.0: version "4.10.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.10.0.tgz#f179737913eaf0d2b98e4926ac1ca6a15cbcc6a9" integrity sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA== @@ -6495,7 +6765,7 @@ browserslist@4.7.0: electron-to-chromium "^1.3.247" node-releases "^1.1.29" -browserslist@^4.12.0: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.3: version "4.13.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== @@ -6524,6 +6794,11 @@ btoa-lite@^1.0.0: resolved "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" @@ -6581,7 +6856,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: version "5.6.0" resolved "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== @@ -6621,6 +6896,11 @@ byte-size@^5.0.1: resolved "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +bytes@1: + version "1.0.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" + integrity sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g= + bytes@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -6691,6 +6971,32 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cacheable-lookup@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" + integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== + dependencies: + "@types/keyv" "^3.1.1" + keyv "^4.0.0" + +cacheable-lookup@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" + integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== + +cacheable-request@^2.1.1: + version "2.1.4" + resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" + integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= + dependencies: + clone-response "1.0.2" + get-stream "3.0.0" + http-cache-semantics "3.8.1" + keyv "3.0.0" + lowercase-keys "1.0.0" + normalize-url "2.0.1" + responselike "1.0.2" + cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" @@ -6704,7 +7010,20 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cachedir@2.3.0: +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + +cachedir@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== @@ -6812,15 +7131,19 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035: - version "1.0.30001035" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz#2bb53b8aa4716b2ed08e088d4dc816a5fe089a1e" - integrity sha512-C1ZxgkuA4/bUEdMbU5WrGY4+UhMFFiXrgNAfxiMIqWgFTWfv/xsZCS2xEHT2LMq7xAZfuAnu6mcqyDl0ZR6wLQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001109: + version "1.0.30001113" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001113.tgz#22016ab55b5a8b04fa00ca342d9ee1b98df48065" + integrity sha512-qMvjHiKH21zzM/VDZr6oosO6Ri3U0V2tC015jRXjOecwQCJtsU5zklTNTk31jQbIOP8gha0h1ccM/g0ECP+4BA== -caniuse-lite@^1.0.30001093: - version "1.0.30001107" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001107.tgz#809360df7a5b3458f627aa46b0f6ed6d5239da9a" - integrity sha512-86rCH+G8onCmdN4VZzJet5uPELII59cUzDphko3thQFgAQG1RNa+sVLDoALIhRYmflo5iSIzWY3vu1XTWtNMQQ== +canvas@^2.6.1: + version "2.6.1" + resolved "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz#0d087dd4d60f5a5a9efa202757270abea8bef89e" + integrity sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA== + dependencies: + nan "^2.14.0" + node-pre-gyp "^0.11.0" + simple-get "^3.0.3" canvg@1.5.3: version "1.5.3" @@ -6849,6 +7172,16 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +caw@^2.0.0, caw@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz#6c3ca071fc194720883c2dc5da9b074bfc7e9e95" + integrity sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA== + dependencies: + get-proxy "^2.0.0" + isurl "^1.0.0-alpha5" + tunnel-agent "^0.6.0" + url-to-options "^1.0.1" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -6877,7 +7210,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -6910,7 +7243,7 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -check-more-types@2.24.0: +check-more-types@2.24.0, check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= @@ -6920,6 +7253,28 @@ check-types@^11.1.1: resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== +cheerio@0.22.0: + version "0.22.0" + resolved "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" + integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4= + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash.assignin "^4.0.9" + lodash.bind "^4.1.4" + lodash.defaults "^4.0.1" + lodash.filter "^4.4.0" + lodash.flatten "^4.2.0" + lodash.foreach "^4.3.0" + lodash.map "^4.4.0" + lodash.merge "^4.4.0" + lodash.pick "^4.2.1" + lodash.reduce "^4.4.0" + lodash.reject "^4.4.0" + lodash.some "^4.4.0" + chokidar@^2.0.4, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -7049,7 +7404,7 @@ cli-spinners@^2.2.0: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== -cli-table3@0.5.1: +cli-table3@0.5.1, cli-table3@~0.5.1: version "0.5.1" resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== @@ -7080,6 +7435,11 @@ cli-width@^2.0.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + clipboard@^2.0.0: version "2.0.6" resolved "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" @@ -7136,7 +7496,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clone-response@^1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -7167,6 +7527,11 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +code-error-fragment@0.0.230: + version "0.0.230" + resolved "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz#d736d75c832445342eca1d1fedbf17d9618b14d7" + integrity sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw== + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -7185,6 +7550,11 @@ codemirror@^5.52.2: resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz#9799121cf8c50809cca487304e9de3a74d33f428" integrity sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA== +coffee-script@^1.12.4: + version "1.12.7" + resolved "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" + integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== + collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" @@ -7251,10 +7621,10 @@ color@^3.0.0, color@^3.1.2: color-convert "^1.9.1" color-string "^1.5.2" -colorette@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz#1f943e5a357fac10b4e0f5aaef3b14cdc1af6ec7" - integrity sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg== +colorette@1.2.1, colorette@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== colornames@^1.1.1: version "1.1.1" @@ -7294,22 +7664,32 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@4.1.1, commander@^4.0.0, commander@^4.0.1, commander@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +command-exists-promise@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/command-exists-promise/-/command-exists-promise-2.0.2.tgz#7beecc4b218299f3c61fa69a4047aa0b36a64a99" + integrity sha512-T6PB6vdFrwnHXg/I0kivM3DqaCGZLjjYSOe0a5WgFKcz1sOnmOeIjnhQPXVXX3QjVbLyTJ85lJkX6lUpukTzaA== -commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@~2.20.3: +commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1, commander@~2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^5.1.0: +commander@^4.0.0, commander@^4.0.1, commander@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^5.0.0, commander@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -common-tags@1.8.0: +commander@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" + integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA== + +common-tags@^1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== @@ -7381,7 +7761,7 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.0, concat-stream@^1.6.2: +concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -7391,7 +7771,7 @@ concat-stream@^1.5.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -concat-stream@^2.0.0, concat-stream@~2.0.0: +concat-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== @@ -7401,7 +7781,7 @@ concat-stream@^2.0.0, concat-stream@~2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" -concat-with-sourcemaps@^1.1.0: +concat-with-sourcemaps@*, concat-with-sourcemaps@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== @@ -7458,11 +7838,21 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +console-stream@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz#a095fe07b20465955f2fafd28b5d72bccd949d44" + integrity sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ= + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= +constate@^1.2.0: + version "1.3.2" + resolved "https://registry.npmjs.org/constate/-/constate-1.3.2.tgz#fa5f0fc292207f1ec21b46a5eb81f59c8b0a8b84" + integrity sha512-aaILV4vXwGTUZaQZHS5F1xBV8wRCR0Ow1505fdkS5/BPg6hbQrhNqdHL4wgxWgaDeEj43mu/Fb+LhqOKTMcrgQ== + contains-path@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" @@ -7473,7 +7863,7 @@ content-disposition@0.5.2: resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= -content-disposition@0.5.3: +content-disposition@0.5.3, content-disposition@^0.5.2: version "0.5.3" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== @@ -7485,6 +7875,11 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +continuable-cache@^0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" + integrity sha1-vXJ6f67XfnH/OYWskzUakSczrQ8= + conventional-changelog-angular@^5.0.3: version "5.0.6" resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.6.tgz#269540c624553aded809c29a3508fdc2b544c059" @@ -7593,7 +7988,7 @@ cookie@0.4.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@^0.4.1: +cookie@^0.4.1, cookie@~0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== @@ -7620,7 +8015,7 @@ copy-descriptor@^0.1.0: resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.2.0: +copy-to-clipboard@^3, copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.1.0, copy-to-clipboard@^3.2.0: version "3.3.1" resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== @@ -7640,12 +8035,12 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a" integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw== -core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.5: +core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.11, core-js@^2.6.5: version "2.6.11" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== -core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0: +core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.5: version "3.6.5" resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== @@ -7738,21 +8133,13 @@ cross-env@^7.0.0: dependencies: cross-spawn "^7.0.1" -cross-fetch@3.0.5: +cross-fetch@3.0.5, cross-fetch@^3.0.4, cross-fetch@^3.0.5: version "3.0.5" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c" integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew== dependencies: node-fetch "2.6.0" -cross-fetch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.4.tgz#7bef7020207e684a7638ef5f2f698e24d9eb283c" - integrity sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw== - dependencies: - node-fetch "2.6.0" - whatwg-fetch "3.0.0" - cross-spawn@6.0.5, cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -7773,6 +8160,15 @@ cross-spawn@7.0.1: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -7782,6 +8178,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +crowdin-cli@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/crowdin-cli/-/crowdin-cli-0.3.0.tgz#eac9989a6fe7feaaf33090397afc187c67b46191" + integrity sha1-6smYmm/n/qrzMJA5evwYfGe0YZE= + dependencies: + request "^2.53.0" + yamljs "^0.2.1" + yargs "^2.3.0" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -7875,7 +8280,7 @@ css-select-base-adapter@^0.1.1: resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== -css-select@^1.1.0: +css-select@^1.1.0, css-select@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= @@ -7938,7 +8343,7 @@ css-what@^3.2.1: resolved "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== -css.escape@^1.5.1: +css.escape@1.5.1, css.escape@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= @@ -8087,71 +8492,71 @@ cyclist@^1.0.1: integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= cypress@*, cypress@^4.2.0: - version "4.9.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-4.9.0.tgz#c188a3864ddf841c0fdc81a9e4eff5cf539cd1c1" - integrity sha512-qGxT5E0j21FPryzhb0OBjCdhoR/n1jXtumpFFSBPYWsaZZhNaBvc3XlBUDEZKkkXPsqUFYiyhWdHN/zo0t5FcA== + version "4.12.1" + resolved "https://registry.npmjs.org/cypress/-/cypress-4.12.1.tgz#0ead1b9f4c0917d69d8b57f996b6e01fe693b6ec" + integrity sha512-9SGIPEmqU8vuRA6xst2CMTYd9sCFCxKSzrHt0wr+w2iAQMCIIsXsQ5Gplns1sT6LDbZcmLv6uehabAOl3fhc9Q== dependencies: - "@cypress/listr-verbose-renderer" "0.4.1" - "@cypress/request" "2.88.5" - "@cypress/xvfb" "1.2.4" - "@types/sinonjs__fake-timers" "6.0.1" - "@types/sizzle" "2.3.2" - arch "2.1.2" - bluebird "3.7.2" - cachedir "2.3.0" - chalk "2.4.2" - check-more-types "2.24.0" - cli-table3 "0.5.1" - commander "4.1.1" - common-tags "1.8.0" - debug "4.1.1" - eventemitter2 "6.4.2" - execa "1.0.0" - executable "4.1.1" - extract-zip "1.7.0" - fs-extra "8.1.0" - getos "3.2.1" - is-ci "2.0.0" - is-installed-globally "0.3.2" - lazy-ass "1.6.0" - listr "0.14.3" - lodash "4.17.15" - log-symbols "3.0.0" - minimist "1.2.5" - moment "2.26.0" - ospath "1.2.2" - pretty-bytes "5.3.0" - ramda "0.26.1" - request-progress "3.0.0" - supports-color "7.1.0" - tmp "0.1.0" - untildify "4.0.0" - url "0.11.0" - yauzl "2.10.0" + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sizzle" "^2.3.2" + arch "^2.1.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^2.4.2" + check-more-types "^2.24.0" + cli-table3 "~0.5.1" + commander "^4.1.1" + common-tags "^1.8.0" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^1.0.0" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^8.1.0" + getos "^3.2.1" + is-ci "^2.0.0" + is-installed-globally "^0.3.2" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.19" + log-symbols "^3.0.0" + minimist "^1.2.5" + moment "^2.27.0" + ospath "^1.2.2" + pretty-bytes "^5.3.0" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.1.0" + tmp "~0.1.0" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" -d3-dispatch@1: - version "1.0.6" - resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" - integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== +"d3-dispatch@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" + integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== d3-force@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/d3-force/-/d3-force-2.0.1.tgz#31750eee8c43535301d571195bf9683beda534e2" - integrity sha512-zh73/N6+MElRojiUG7vmn+3vltaKon7iD5vB/7r9nUaBeftXMzRo5IWEG63DLBCto4/8vr9i3m9lwr1OTJNiCg== + version "2.1.1" + resolved "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz#f20ccbf1e6c9e80add1926f09b51f686a8bc0937" + integrity sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew== dependencies: - d3-dispatch "1" - d3-quadtree "1" - d3-timer "1" + d3-dispatch "1 - 2" + d3-quadtree "1 - 2" + d3-timer "1 - 2" -d3-quadtree@1: - version "1.0.7" - resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135" - integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA== +"d3-quadtree@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz#edbad045cef88701f6fee3aee8e93fb332d30f9d" + integrity sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw== -d3-timer@1: - version "1.0.10" - resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" - integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== +"d3-timer@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" + integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== d@1, d@^1.0.1: version "1.0.1" @@ -8208,7 +8613,7 @@ date-fns@^1.27.2: resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== -date-fns@^2.0.0-alpha.27, date-fns@^2.0.1: +date-fns@^2.0.0-alpha.27, date-fns@^2.0.1, date-fns@^2.15.0: version "2.15.0" resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ== @@ -8237,6 +8642,13 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" +debug@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" + integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg== + dependencies: + ms "^2.1.1" + debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -8279,13 +8691,87 @@ decode-uri-component@^0.2.0: resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -decompress-response@^3.3.0: +decompress-response@^3.2.0, decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= dependencies: mimic-response "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +decompress-response@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" + integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== + dependencies: + mimic-response "^2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.0.0, decompress@^4.2.0: + version "4.2.1" + resolved "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -8303,7 +8789,7 @@ deep-equal@^1.0.1, deep-equal@^1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-extend@^0.6.0: +deep-extend@0.6.0, deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -8348,6 +8834,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -8501,6 +8992,11 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +diacritics-map@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz#6dfc0ff9d01000a2edf2865371cac316e94977af" + integrity sha1-bfwP+dAQAKLt8oZTccrDFulJd68= + diagnostics@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a" @@ -8522,10 +9018,10 @@ diff-sequences@^25.2.6: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== -diff-sequences@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6" - integrity sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg== +diff-sequences@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.3.0.tgz#62a59b1b29ab7fd27cef2a33ae52abe73042d0a2" + integrity sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig== diff@^4.0.1, diff@^4.0.2: version "4.0.2" @@ -8593,16 +9089,7 @@ docker-modem@^2.1.0: split-ca "^1.0.1" ssh2 "^0.8.7" -dockerode@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/dockerode/-/dockerode-3.2.0.tgz#80e5bd9c2a988bdde4c995ae4aa2584fa0166c23" - integrity sha512-C+y/W4Kks7YLBsfUOTMkk1IVilb4cdj+rE+UZ5hnE+rpcn2frSs7kX+6H8GteTqHcv8sln+GyxuP1qdno3IzIw== - dependencies: - concat-stream "~2.0.0" - docker-modem "^2.1.0" - tar-fs "~2.0.1" - -dockerode@^3.2.1: +dockerode@^3.2.0, dockerode@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/dockerode/-/dockerode-3.2.1.tgz#4a2222e3e1df536bf595e78e76d3cfbf6d4d93b9" integrity sha512-XsSVB5Wu5HWMg1aelV5hFSqFJaKS5x1aiV/+sT7YOzOq1IRl49I/UwV8Pe4x6t0iF9kiGkWu5jwfvbkcFVupBw== @@ -8632,10 +9119,62 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.4.5: - version "0.4.5" - resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz#d9c1cefa89f509d8cf132ab5d250004d755e76e3" - integrity sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg== +docusaurus@^2.0.0-alpha.61: + version "2.0.0-alpha.61" + resolved "https://registry.npmjs.org/docusaurus/-/docusaurus-2.0.0-alpha.61.tgz#f347b81c98c66f1de3ecfccf63fa421ddff52fbb" + integrity sha512-qDU3nOA4Xs95tIjjSETnEuRmTukTzgxyTZ5MgMyuG7y6h4oDHtpLcYf8F+xlXuuWHKv3VSxRJNzV8fZHPgnK3g== + dependencies: + "@babel/core" "^7.9.0" + "@babel/plugin-proposal-class-properties" "^7.8.3" + "@babel/plugin-proposal-object-rest-spread" "^7.9.0" + "@babel/polyfill" "^7.8.7" + "@babel/preset-env" "^7.9.0" + "@babel/preset-react" "^7.9.4" + "@babel/register" "^7.9.0" + "@babel/traverse" "^7.9.0" + "@babel/types" "^7.9.0" + autoprefixer "^9.7.5" + babylon "^6.18.0" + chalk "^3.0.0" + classnames "^2.2.6" + commander "^4.0.1" + crowdin-cli "^0.3.0" + cssnano "^4.1.10" + escape-string-regexp "^2.0.0" + express "^4.17.1" + feed "^4.0.0" + fs-extra "^8.1.0" + gaze "^1.1.3" + github-slugger "^1.2.1" + glob "^7.1.6" + highlight.js "^9.16.2" + imagemin "^6.0.0" + imagemin-gifsicle "^6.0.1" + imagemin-jpegtran "^6.0.0" + imagemin-optipng "^6.0.0" + imagemin-svgo "^7.0.0" + lodash "^4.17.15" + markdown-toc "^1.2.0" + mkdirp "^0.5.1" + portfinder "^1.0.25" + postcss "^7.0.23" + prismjs "^1.17.1" + react "^16.8.4" + react-dev-utils "^9.1.0" + react-dom "^16.8.4" + remarkable "^2.0.0" + request "^2.88.0" + shelljs "^0.8.4" + sitemap "^3.2.2" + tcp-port-used "^1.0.1" + tiny-lr "^1.1.1" + tree-node-cli "^1.2.5" + truncate-html "^1.0.3" + +dom-accessibility-api@^0.5.1: + version "0.5.2" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.2.tgz#ef3cdb5d3f0d599d8f9c8b18df2fb63c9793739d" + integrity sha512-k7hRNKAiPJXD2aBqfahSo4/01cTsKWXf+LqJgglnkN2Nz8TsxXKQBXHhKe0Ye9fEfHEZY49uSA5Sr3AqP/sWKA== dom-converter@^0.2: version "0.2.0" @@ -8644,7 +9183,7 @@ dom-converter@^0.2: dependencies: utila "~0.4" -dom-helpers@^5.0.0: +dom-helpers@^5.0.0, dom-helpers@^5.0.1: version "5.1.4" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== @@ -8652,14 +9191,6 @@ dom-helpers@^5.0.0: "@babel/runtime" "^7.8.7" csstype "^2.6.7" -dom-helpers@^5.0.1: - version "5.1.3" - resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" - integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== - dependencies: - "@babel/runtime" "^7.6.3" - csstype "^2.6.7" - dom-serializer@0, dom-serializer@^0.2.1: version "0.2.2" resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -8668,6 +9199,14 @@ dom-serializer@0, dom-serializer@^0.2.1: domelementtype "^2.0.1" entities "^2.0.0" +dom-serializer@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -8678,7 +9217,7 @@ domain-browser@^1.1.1: resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -8716,6 +9255,16 @@ domhandler@^3.0, domhandler@^3.0.0: dependencies: domelementtype "^2.0.1" +dompurify@^1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-1.0.11.tgz#fe0f4a40d147f7cebbe31a50a1357539cfc1eb4d" + integrity sha512-XywCTXZtc/qCX3iprD1pIklRVk/uhl8BKpkTxr+ZyMVUzSUg7wkQXRBp/euJ5J5moa1QvfpvaPQVP71z1O59dQ== + +dompurify@^2.0.7: + version "2.0.12" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.0.12.tgz#284a2b041e1c60b8e72d7b4d2fadad36141254ae" + integrity sha512-Fl8KseK1imyhErHypFPA8qpq9gPzlsJ/EukA6yk9o0gX23p1TzC+rh9LqNg1qvErRTc0UNMYlKxEGSfSh43NDg== + domutils@1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -8799,6 +9348,41 @@ dotenv@^8.0.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== +download@^6.2.2: + version "6.2.5" + resolved "https://registry.npmjs.org/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714" + integrity sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA== + dependencies: + caw "^2.0.0" + content-disposition "^0.5.2" + decompress "^4.0.0" + ext-name "^5.0.0" + file-type "5.2.0" + filenamify "^2.0.0" + get-stream "^3.0.0" + got "^7.0.0" + make-dir "^1.0.0" + p-event "^1.0.0" + pify "^3.0.0" + +download@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/download/-/download-7.1.0.tgz#9059aa9d70b503ee76a132897be6dec8e5587233" + integrity sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ== + dependencies: + archive-type "^4.0.0" + caw "^2.0.1" + content-disposition "^0.5.2" + decompress "^4.2.0" + ext-name "^5.0.0" + file-type "^8.1.0" + filenamify "^2.0.0" + get-stream "^3.0.0" + got "^8.3.1" + make-dir "^1.2.0" + p-event "^2.1.0" + pify "^3.0.0" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -8837,12 +9421,7 @@ ejs@^2.7.4: resolved "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378: - version "1.3.380" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.380.tgz#1e1f07091b42b54bccd0ad6d3a14f2b73b60dc9d" - integrity sha512-2jhQxJKcjcSpVOQm0NAfuLq8o+130blrcawoumdXT6411xG/xIAOyZodO/y7WTaYlz/NHe3sCCAe/cJLnDsqTw== - -electron-to-chromium@^1.3.488: +electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.488: version "1.3.509" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.509.tgz#830fcb89cd66dc2984d18d794973b99e3f00584c" integrity sha512-cN4lkjNRuTG8rtAqTOVgwpecEC2kbKA04PG6YijcKGHK/kD0xLjiqExcAOmLUwtXZRF8cBeam2I0VZcih919Ug== @@ -8872,6 +9451,16 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emitter-component@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6" + integrity sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY= + +emittery@^0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451" + integrity sha512-d34LN4L6h18Bzz9xpoku2nPwKxCPlPMr3EEKTkoEBi+1/+b0lcRkRJ1UVyyZaKNeqGR3swcGl6s390DNO4YVgQ== + "emoji-regex@>=6.0.0 <=6.1.1": version "6.1.1" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" @@ -8948,7 +9537,7 @@ enquirer@^2.3.5: dependencies: ansi-colors "^3.2.1" -entities@^1.1.1, entities@^1.1.2: +entities@^1.1.1, entities@^1.1.2, entities@~1.1.1: version "1.1.2" resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -8999,6 +9588,13 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" +error@^7.0.0: + version "7.2.1" + resolved "https://registry.npmjs.org/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894" + integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA== + dependencies: + string-template "~0.2.1" + es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: version "1.17.4" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" @@ -9043,7 +9639,7 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.50: +es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: version "0.10.53" resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== @@ -9057,7 +9653,7 @@ es5-shim@^4.5.13: resolved "https://registry.npmjs.org/es5-shim/-/es5-shim-4.5.13.tgz#5d88062de049f8969f83783f4a4884395f21d28b" integrity sha512-xi6hh6gsvDE0MaW4Vp1lgNEBpVcCXRWfPXj5egDvtgLz4L9MEvNwYEMdJH+JJinWkwa8c3c3o5HduV7dB/e1Hw== -es6-iterator@~2.0.3: +es6-iterator@^2.0.3, es6-iterator@~2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= @@ -9091,6 +9687,16 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3: d "^1.0.1" ext "^1.1.2" +es6-weak-map@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + esbuild@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.6.3.tgz#a957e22f2503c745793514388110f9b55b3a6f83" @@ -9121,19 +9727,7 @@ escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.14.1, escodegen@^1.9.1: - version "1.14.1" - resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" - integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -escodegen@^1.6.1: +escodegen@^1.14.1, escodegen@^1.6.1, escodegen@^1.9.1: version "1.14.3" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== @@ -9422,6 +10016,14 @@ etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + event-stream@=3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" @@ -9435,10 +10037,10 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -eventemitter2@6.4.2: - version "6.4.2" - resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" - integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== +eventemitter2@^6.4.2: + version "6.4.3" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" + integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== eventemitter3@^3.1.0: version "3.1.2" @@ -9470,24 +10072,22 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +exec-buffer@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz#b1686dbd904c7cf982e652c1f5a79b1e5573082b" + integrity sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA== + dependencies: + execa "^0.7.0" + p-finally "^1.0.0" + pify "^3.0.0" + rimraf "^2.5.4" + tempfile "^2.0.0" + exec-sh@^0.3.2: version "0.3.4" resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== -execa@1.0.0, execa@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -9504,6 +10104,32 @@ execa@3.4.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^4.0.0, execa@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" @@ -9519,7 +10145,7 @@ execa@^4.0.0, execa@^4.0.1: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -executable@4.1.1: +executable@^4.1.0, executable@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== @@ -9549,6 +10175,13 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= + dependencies: + fill-range "^2.1.0" + expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -9556,18 +10189,26 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/expect/-/expect-26.0.1.tgz#18697b9611a7e2725e20ba3ceadda49bc9865421" - integrity sha512-QcCy4nygHeqmbw564YxNbHTJlXh47dVID2BUP52cZFpLU9zHViMFK6h07cC1wf7GYCTIigTdAXhVua8Yl1FkKg== +expect@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/expect/-/expect-26.4.2.tgz#36db120928a5a2d7d9736643032de32f24e1b2a1" + integrity sha512-IlJ3X52Z0lDHm7gjEp+m76uX46ldH5VpqmU0006vqDju/285twh7zaWMRhs67VpQhBwjjMchk+p5aA0VkERCAA== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" ansi-styles "^4.0.0" - jest-get-type "^26.0.0" - jest-matcher-utils "^26.0.1" - jest-message-util "^26.0.1" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.4.2" + jest-message-util "^26.3.0" jest-regex-util "^26.0.0" +express-prom-bundle@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.1.0.tgz#8fd72e5bedbbd686b8e7c49e0aecbc1b14b28743" + integrity sha512-krlvp5r6sgJ1IwL6M6/coMrNbAlwtpk+uyivfeyRMCupTK4HzIEQHH0gwrNhLiKyPmSbtZcSmtO6s+PRumRp5g== + dependencies: + on-finished "^2.3.0" + url-value-parser "^2.0.0" + express-promise-router@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70" @@ -9613,6 +10254,21 @@ express@^4.0.0, express@^4.17.0, express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext-list@^2.0.0: + version "2.2.2" + resolved "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" + integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== + dependencies: + mime-db "^1.28.0" + +ext-name@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" + integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== + dependencies: + ext-list "^2.0.0" + sort-keys-length "^1.0.0" + ext@^1.1.2: version "1.4.0" resolved "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" @@ -9663,7 +10319,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.7.0: +extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== @@ -9688,6 +10344,11 @@ fast-deep-equal@2.0.1, fast-deep-equal@^2.0.1: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= + fast-deep-equal@^3.0.0, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -9717,6 +10378,13 @@ fast-glob@^3.0.3, fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" +fast-json-patch@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz#18150d36c9ab65c7209e7d4eb113f4f8eaabe6d9" + integrity sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -9768,7 +10436,7 @@ fault@^1.0.0, fault@^1.0.2: dependencies: format "^0.2.0" -faye-websocket@^0.10.0: +faye-websocket@^0.10.0, faye-websocket@~0.10.0: version "0.10.0" resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= @@ -9801,6 +10469,13 @@ fecha@^2.3.3: resolved "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== +feed@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/feed/-/feed-4.2.1.tgz#b246ef891051c7dbf088ca203341d9fb0444baee" + integrity sha512-l28KKcK1J/u3iq5dRDmmoB2p7dtBfACC2NqJh4dI2kFptxH0asfjmOfcxqh5Sv8suAlVa73gZJ4REY5RrafVvg== + dependencies: + xml-js "^1.6.11" + fetch-readablestream@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" @@ -9811,7 +10486,7 @@ figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== -figures@^1.7.0: +figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= @@ -9861,6 +10536,36 @@ file-system-cache@^1.0.5: fs-extra "^0.30.0" ramda "^0.21.0" +file-type@5.2.0, file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha1-LdvqfHP/42No365J3DOMBYwritY= + +file-type@^10.4.0, file-type@^10.7.0: + version "10.11.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz#2961d09e4675b9fb9a3ee6b69e9cd23f43fd1890" + integrity sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw== + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= + +file-type@^4.2.0: + version "4.4.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" + integrity sha1-G2AOX8ofvcboDApwxxyNul95BsU= + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== + +file-type@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz#244f3b7ef641bbe0cca196c7276e4b332399f68c" + integrity sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -9871,6 +10576,20 @@ filefy@0.1.10: resolved "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz#174677c8e2fa5bc39a3af0ed6fb492f16b8fbf42" integrity sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ== +filename-reserved-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" + integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik= + +filenamify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz#88faf495fb1b47abfd612300002a16228c677ee9" + integrity sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.0" + trim-repeated "^1.0.0" + filesize@3.6.1: version "3.6.1" resolved "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" @@ -9881,6 +10600,17 @@ filesize@6.0.1: resolved "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz#f850b509909c7c86f7e450ea19006c31c2ed3d2f" integrity sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg== +fill-range@^2.1.0: + version "2.2.4" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" + integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^3.0.0" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -9911,7 +10641,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^2.1.0: +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -9964,7 +10694,7 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" -find-versions@^3.2.0: +find-versions@^3.0.0, find-versions@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== @@ -10036,12 +10766,10 @@ follow-redirects@1.5.10: dependencies: debug "=3.1.0" -follow-redirects@^1.0.0: - version "1.10.0" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb" - integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ== - dependencies: - debug "^3.0.0" +follow-redirects@^1.0.0, follow-redirects@^1.10.0: + version "1.13.0" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== for-in@^0.1.3: version "0.1.8" @@ -10067,6 +10795,11 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" +foreach@^2.0.4: + version "2.0.5" + resolved "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -10113,7 +10846,7 @@ fork-ts-checker-webpack-plugin@^4.0.5: tapable "^1.0.0" worker-rpc "^0.1.0" -form-data@^2.3.1: +form-data@^2.3.1, form-data@^2.3.2: version "2.5.1" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -10167,7 +10900,7 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^2.1.0: +from2@^2.1.0, from2@^2.1.1: version "2.3.0" resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= @@ -10199,7 +10932,7 @@ fs-extra@8.1.0, fs-extra@^8.0.1, fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@9.0.1, fs-extra@^9.0.1: +fs-extra@9.0.1, fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== @@ -10229,16 +10962,6 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" - integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^1.0.0" - fs-minipass@^1.2.5: version "1.2.7" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -10324,6 +11047,13 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +gaze@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + generic-names@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" @@ -10390,6 +11120,13 @@ get-port@^5.1.1: resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== +get-proxy@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz#349f2b4d91d44c4d4d4e9cba2ad90143fac5ef93" + integrity sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw== + dependencies: + npm-conf "^1.1.0" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -10400,6 +11137,19 @@ get-stdin@^6.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== +get-stream@3.0.0, get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -10424,7 +11174,7 @@ getopts@2.2.5: resolved "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== -getos@3.2.1: +getos@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== @@ -10438,6 +11188,16 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gifsicle@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/gifsicle/-/gifsicle-4.0.1.tgz#30e1e61e3ee4884ef702641b2e98a15c2127b2e2" + integrity sha512-A/kiCLfDdV+ERV/UB+2O41mifd+RxH8jlRG8DMxZO84Bma/Fw0htqZ+hY2iaalLRNyUu7tYZQslqUBJxBggxbg== + dependencies: + bin-build "^3.0.0" + bin-wrapper "^4.0.0" + execa "^1.0.0" + logalot "^2.0.0" + git-raw-commits@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" @@ -10473,10 +11233,10 @@ git-up@^4.0.0: is-ssh "^1.3.0" parse-url "^5.0.0" -git-url-parse@^11.1.2: - version "11.1.2" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.2.tgz#aff1a897c36cc93699270587bea3dbcbbb95de67" - integrity sha512-gZeLVGY8QVKMIkckncX+iCq2/L8PlwncvDFKiWkBn9EtCfYDbliRTTp6qzyQ1VMdITUfq7293zDzfpjdiGASSQ== +git-url-parse@^11.1.2, git-url-parse@^11.1.3: + version "11.1.3" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.3.tgz#03625b6fc09905e9ad1da7bb2b84be1bf9123143" + integrity sha512-GPsfwticcu52WQ+eHp0IYkAyaOASgYdtsQDIt4rUp6GbiNt1P9ddrh3O0kQB0eD4UJZszVqNT3+9Zwcg40fywA== dependencies: git-up "^4.0.0" @@ -10487,7 +11247,7 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -github-slugger@^1.3.0: +github-slugger@^1.2.1, github-slugger@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q== @@ -10529,7 +11289,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -10623,7 +11383,7 @@ globby@11.0.1, globby@^11.0.0: merge2 "^1.3.0" slash "^3.0.0" -globby@8.0.2: +globby@8.0.2, globby@^8.0.1: version "8.0.2" resolved "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w== @@ -10687,6 +11447,15 @@ globby@^9.2.0: pify "^4.0.1" slash "^2.0.0" +globule@^1.0.0: + version "1.3.2" + resolved "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4" + integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA== + dependencies: + glob "~7.1.1" + lodash "~4.17.10" + minimatch "~3.0.2" + good-listener@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" @@ -10694,6 +11463,87 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" +got@^10.7.0: + version "10.7.0" + resolved "https://registry.npmjs.org/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" + integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== + dependencies: + "@sindresorhus/is" "^2.0.0" + "@szmarczak/http-timer" "^4.0.0" + "@types/cacheable-request" "^6.0.1" + cacheable-lookup "^2.0.0" + cacheable-request "^7.0.1" + decompress-response "^5.0.0" + duplexer3 "^0.1.4" + get-stream "^5.0.0" + lowercase-keys "^2.0.0" + mimic-response "^2.1.0" + p-cancelable "^2.0.0" + p-event "^4.0.0" + responselike "^2.0.0" + to-readable-stream "^2.0.0" + type-fest "^0.10.0" + +got@^11.5.2: + version "11.6.0" + resolved "https://registry.npmjs.org/got/-/got-11.6.0.tgz#4978c78f3cbc3a45ee95381f8bb6efd1db1f4638" + integrity sha512-ErhWb4IUjQzJ3vGs3+RR12NWlBDDkRciFpAkQ1LPUxi6OnwhGj07gQxjPsyIk69s7qMihwKrKquV6VQq7JNYLA== + dependencies: + "@sindresorhus/is" "^3.1.1" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + +got@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" + integrity sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw== + dependencies: + decompress-response "^3.2.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-plain-obj "^1.1.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + p-cancelable "^0.3.0" + p-timeout "^1.1.1" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + url-parse-lax "^1.0.0" + url-to-options "^1.0.1" + +got@^8.3.1: + version "8.3.2" + resolved "https://registry.npmjs.org/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" + integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== + dependencies: + "@sindresorhus/is" "^0.7.0" + cacheable-request "^2.1.1" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + into-stream "^3.1.0" + is-retry-allowed "^1.1.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + mimic-response "^1.0.0" + p-cancelable "^0.4.0" + p-timeout "^2.0.1" + pify "^3.0.0" + safe-buffer "^5.1.1" + timed-out "^4.0.1" + url-parse-lax "^3.0.0" + url-to-options "^1.0.1" + got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -10711,16 +11561,16 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.2.4: +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + graphiql@^1.0.0-alpha.10: version "1.0.3" resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.0.3.tgz#f6b5d5c417d8f1a28786d3228a69883426ba74ad" @@ -10830,10 +11680,10 @@ graphql-upload@^8.0.2: http-errors "^1.7.3" object-path "^0.11.4" -graphql@15.1.0, graphql@^15.0.0: - version "15.1.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.1.0.tgz#b93e28de805294ec08e1630d901db550cb8960a1" - integrity sha512-0TVyfOlCGhv/DBczQkJmwXOK6fjWkjzY3Pt7wY8i0gcYXq8aogG3weCsg48m72lywKSeOqedEHvVPOvZvSD51Q== +graphql@15.3.0, graphql@^15.0.0, graphql@^15.3.0: + version "15.3.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278" + integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w== graphql@^14.5.3: version "14.7.0" @@ -10842,10 +11692,16 @@ graphql@^14.5.3: dependencies: iterall "^1.2.2" -graphql@^15.3.0: - version "15.3.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278" - integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w== +gray-matter@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e" + integrity sha1-MELZrewqHe1qdwep7SOA+KF6Qw4= + dependencies: + ansi-red "^0.1.1" + coffee-script "^1.12.4" + extend-shallow "^2.0.1" + js-yaml "^3.8.1" + toml "^2.3.2" growly@^1.3.0: version "1.3.0" @@ -10857,6 +11713,15 @@ gud@^1.0.0: resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== +gulp-header@^1.7.1: + version "1.8.12" + resolved "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" + integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ== + dependencies: + concat-with-sourcemaps "*" + lodash.template "^4.4.0" + through2 "^2.0.0" + gzip-size@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -10922,11 +11787,23 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-symbol-support-x@^1.4.1: + version "1.4.2" + resolved "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" + integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== + has-symbols@^1.0.0, has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-to-string-tag-x@^1.2.0: + version "1.4.1" + resolved "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" + integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== + dependencies: + has-symbol-support-x "^1.4.1" + has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -11031,11 +11908,21 @@ highlight.js@^10.1.1, highlight.js@~10.1.0: resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.1.2.tgz#c20db951ba1c22c055010648dfffd7b2a968e00c" integrity sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA== +highlight.js@^9.16.2: + version "9.18.3" + resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" + integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== + highlight.js@~9.13.0: version "9.13.1" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== +highlight.js@~9.15.0, highlight.js@~9.15.1: + version "9.15.10" + resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" + integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== + history@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" @@ -11096,7 +11983,7 @@ hsla-regex@^1.0.0: resolved "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= -html-comment-regex@^1.1.0: +html-comment-regex@^1.1.0, html-comment-regex@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== @@ -11170,7 +12057,7 @@ html2canvas@1.0.0-alpha.12: dependencies: css-line-break "1.0.1" -htmlparser2@^3.3.0: +htmlparser2@^3.3.0, htmlparser2@^3.9.1: version "3.10.1" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -11192,7 +12079,7 @@ htmlparser2@^4.0, htmlparser2@^4.1.0: domutils "^2.0.0" entities "^2.0.0" -http-cache-semantics@^3.8.1: +http-cache-semantics@3.8.1, http-cache-semantics@^3.8.1: version "3.8.1" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== @@ -11283,16 +12170,7 @@ http-proxy-middleware@^0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0: - version "1.18.0" - resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" - integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-proxy@^1.18.1: +http-proxy@^1.17.0, http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== @@ -11310,6 +12188,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.0-beta.5.2" + resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" + integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -11382,7 +12268,7 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.4: +ieee754@^1.1.13, ieee754@^1.1.4: version "1.1.13" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== @@ -11419,12 +12305,59 @@ ignore@^5.1.1, ignore@^5.1.4: resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== +imagemin-gifsicle@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-6.0.1.tgz#6abad4e95566d52e5a104aba1c24b4f3b48581b3" + integrity sha512-kuu47c6iKDQ6R9J10xCwL0lgs0+sMz3LRHqRcJ2CRBWdcNmo3T5hUaM8hSZfksptZXJLGKk8heSAvwtSdB1Fng== + dependencies: + exec-buffer "^3.0.0" + gifsicle "^4.0.0" + is-gif "^3.0.0" + +imagemin-jpegtran@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-6.0.0.tgz#c8d3bcfb6ec9c561c20a987142854be70d90b04f" + integrity sha512-Ih+NgThzqYfEWv9t58EItncaaXIHR0u9RuhKa8CtVBlMBvY0dCIxgQJQCfwImA4AV1PMfmUKlkyIHJjb7V4z1g== + dependencies: + exec-buffer "^3.0.0" + is-jpg "^2.0.0" + jpegtran-bin "^4.0.0" + +imagemin-optipng@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-6.0.0.tgz#a6bfc7b542fc08fc687e83dfb131249179a51a68" + integrity sha512-FoD2sMXvmoNm/zKPOWdhKpWdFdF9qiJmKC17MxZJPH42VMAp17/QENI/lIuP7LCUnLVAloO3AUoTSNzfhpyd8A== + dependencies: + exec-buffer "^3.0.0" + is-png "^1.0.0" + optipng-bin "^5.0.0" + +imagemin-svgo@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-7.1.0.tgz#528a42fd3d55eff5d4af8fd1113f25fb61ad6d9a" + integrity sha512-0JlIZNWP0Luasn1HT82uB9nU9aa+vUj6kpT+MjPW11LbprXC+iC4HDwn1r4Q2/91qj4iy9tRZNsFySMlEpLdpg== + dependencies: + is-svg "^4.2.1" + svgo "^1.3.2" + +imagemin@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/imagemin/-/imagemin-6.1.0.tgz#62508b465728fea36c03cdc07d915fe2d8cf9e13" + integrity sha512-8ryJBL1CN5uSHpiBMX0rJw79C9F9aJqMnjGnrd/1CafegpNuA81RBAAru/jQQEOWlOJJlpRnlcVFF6wq+Ist0A== + dependencies: + file-type "^10.7.0" + globby "^8.0.1" + make-dir "^1.0.0" + p-pipe "^1.1.0" + pify "^4.0.1" + replace-ext "^1.0.0" + immer@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== -immutable@>=3.8.2, immutable@^3.8.2: +immutable@>=3.8.2, immutable@^3.8.1, immutable@^3.8.2, immutable@^3.x.x: version "3.8.2" resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= @@ -11478,6 +12411,11 @@ import-lazy@^2.1.0: resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= +import-lazy@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" + integrity sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ== + import-local@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" @@ -11634,20 +12572,20 @@ inquirer@^6.2.0: through "^2.3.6" inquirer@^7.0.0, inquirer@^7.0.4: - version "7.2.0" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.2.0.tgz#63ce99d823090de7eb420e4bb05e6f3449aa389a" - integrity sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ== + version "7.3.3" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== dependencies: ansi-escapes "^4.2.1" - chalk "^3.0.0" + chalk "^4.1.0" cli-cursor "^3.1.0" - cli-width "^2.0.0" + cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.15" + lodash "^4.17.19" mute-stream "0.0.8" run-async "^2.4.0" - rxjs "^6.5.3" + rxjs "^6.6.0" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" @@ -11674,10 +12612,18 @@ interpret@^1.0.0: resolved "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -interpret@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/interpret/-/interpret-2.0.0.tgz#b783ffac0b8371503e9ab39561df223286aa5433" - integrity sha512-e0/LknJ8wpMMhTiWcjivB+ESwIuvHnBSlBbmP/pSb8CQJldoj1p2qv7xGZ/+BtbTziYRFSz8OsvdbiX45LtYQA== +interpret@^2.0.0, interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +into-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" + integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= + dependencies: + from2 "^2.1.1" + p-is-promise "^1.1.0" invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" @@ -11790,7 +12736,7 @@ is-callable@^1.1.4, is-callable@^1.1.5: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== -is-ci@2.0.0, is-ci@^2.0.0: +is-ci@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== @@ -11913,16 +12859,23 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-function@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" - integrity sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU= +is-function@^1.0.1, is-function@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" + integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== is-generator-fn@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-gif@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz#c4be60b26a301d695bb833b20d9b5d66c6cf83b1" + integrity sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw== + dependencies: + file-type "^10.4.0" + is-glob@4.0.1, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -11954,7 +12907,7 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= -is-installed-globally@0.3.2, is-installed-globally@^0.3.1: +is-installed-globally@^0.3.1, is-installed-globally@^0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== @@ -11967,6 +12920,11 @@ is-interactive@^1.0.0: resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-jpg@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz#2e1997fa6e9166eaac0242daae443403e4ef1d97" + integrity sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc= + is-map@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" @@ -11977,11 +12935,23 @@ is-module@^1.0.0: resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= + is-npm@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= + dependencies: + kind-of "^3.0.2" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -11989,6 +12959,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -12059,15 +13034,20 @@ is-plain-object@^3.0.0: dependencies: isobject "^4.0.0" +is-png@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce" + integrity sha1-1XSxK/J1wDUEVVcLDltXqwYgd84= + is-potential-custom-element-name@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= +is-promise@^2.1, is-promise@^2.1.0: + version "2.2.2" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== is-reference@^1.1.2: version "1.1.4" @@ -12076,12 +13056,12 @@ is-reference@^1.1.2: dependencies: "@types/estree" "0.0.39" -is-regex@^1.0.4, is-regex@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" - integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== +is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== dependencies: - has "^1.0.3" + has-symbols "^1.0.1" is-regexp@^1.0.0: version "1.0.0" @@ -12100,6 +13080,11 @@ is-resolvable@^1.0.0: resolved "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== +is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + is-root@2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" @@ -12117,7 +13102,7 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" -is-stream@^1.1.0: +is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -12139,6 +13124,13 @@ is-svg@^3.0.0: dependencies: html-comment-regex "^1.1.0" +is-svg@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/is-svg/-/is-svg-4.2.1.tgz#095b496e345fec9211c2a7d5d021003e040d6f81" + integrity sha512-PHx3ANecKsKNl5y5+Jvt53Y4J7MfMpbNZkv384QNiswMKAWIbvcqbPz+sYbFKJI8Xv3be01GSFniPmoaP+Ai5A== + dependencies: + html-comment-regex "^1.1.2" + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -12165,6 +13157,11 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" +is-url@^1.2.2: + version "1.2.4" + resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -12200,11 +13197,27 @@ is-wsl@^2.1.1: resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d" integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== +is2@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz#8ac355644840921ce435d94f05d3a94634d3481a" + integrity sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA== + dependencies: + deep-is "^0.1.3" + ip-regex "^2.1.0" + is-url "^1.2.2" + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -12237,6 +13250,13 @@ isobject@^4.0.0: resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== +isomorphic-form-data@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz#9f6adf1c4c61ae3aefd8f110ab60fb9b143d6cec" + integrity sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg== + dependencies: + form-data "^2.3.2" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -12260,6 +13280,16 @@ istanbul-lib-instrument@^4.0.0: istanbul-lib-coverage "^3.0.0" semver "^6.3.0" +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" @@ -12286,6 +13316,14 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +isurl@^1.0.0-alpha5: + version "1.0.0" + resolved "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" + integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== + dependencies: + has-to-string-tag-x "^1.2.0" + is-object "^1.0.1" + iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: version "1.3.0" resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" @@ -12304,57 +13342,64 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -jest-changed-files@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.0.1.tgz#1334630c6a1ad75784120f39c3aa9278e59f349f" - integrity sha512-q8LP9Sint17HaE2LjxQXL+oYWW/WeeXMPE2+Op9X3mY8IEGFVc14xRxFjUuXUbcPAlDLhtWdIEt59GdQbn76Hw== +jenkins@^0.28.0: + version "0.28.0" + resolved "https://registry.npmjs.org/jenkins/-/jenkins-0.28.0.tgz#72d6fcc452145403b34f6d4ecbd877ee1ab77fca" + integrity sha512-EGzzZcyFwXBCPZZoDNvZTPmZOqaHALfAStNfCF37oh+Mc6G/e0MwIuQjx5kMEynTXR9bF5EwLiuMTiTf5kHk5g== dependencies: - "@jest/types" "^26.0.1" + papi "^0.29.0" + +jest-changed-files@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.3.0.tgz#68fb2a7eb125f50839dab1f5a17db3607fe195b1" + integrity sha512-1C4R4nijgPltX6fugKxM4oQ18zimS7LqQ+zTTY8lMCMFPrxqBFb7KJH0Z2fRQJvw2Slbaipsqq7s1mgX5Iot+g== + dependencies: + "@jest/types" "^26.3.0" execa "^4.0.0" throat "^5.0.0" -jest-cli@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-26.0.1.tgz#3a42399a4cbc96a519b99ad069a117d955570cac" - integrity sha512-pFLfSOBcbG9iOZWaMK4Een+tTxi/Wcm34geqZEqrst9cZDkTQ1LZ2CnBrTlHWuYAiTMFr0EQeK52ScyFU8wK+w== +jest-cli@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-26.4.2.tgz#24afc6e4dfc25cde4c7ec4226fb7db5f157c21da" + integrity sha512-zb+lGd/SfrPvoRSC/0LWdaWCnscXc1mGYW//NP4/tmBvRPT3VntZ2jtKUONsRi59zc5JqmsSajA9ewJKFYp8Cw== dependencies: - "@jest/core" "^26.0.1" - "@jest/test-result" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/core" "^26.4.2" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^26.0.1" - jest-util "^26.0.1" - jest-validate "^26.0.1" + jest-config "^26.4.2" + jest-util "^26.3.0" + jest-validate "^26.4.2" prompts "^2.0.1" yargs "^15.3.1" -jest-config@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-config/-/jest-config-26.0.1.tgz#096a3d4150afadf719d1fab00e9a6fb2d6d67507" - integrity sha512-9mWKx2L1LFgOXlDsC4YSeavnblN6A4CPfXFiobq+YYLaBMymA/SczN7xYTSmLaEYHZOcB98UdoN4m5uNt6tztg== +jest-config@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-26.4.2.tgz#da0cbb7dc2c131ffe831f0f7f2a36256e6086558" + integrity sha512-QBf7YGLuToiM8PmTnJEdRxyYy3mHWLh24LJZKVdXZ2PNdizSe1B/E8bVm+HYcjbEzGuVXDv/di+EzdO/6Gq80A== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.0.1" - "@jest/types" "^26.0.1" - babel-jest "^26.0.1" + "@jest/test-sequencer" "^26.4.2" + "@jest/types" "^26.3.0" + babel-jest "^26.3.0" chalk "^4.0.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-environment-jsdom "^26.0.1" - jest-environment-node "^26.0.1" - jest-get-type "^26.0.0" - jest-jasmine2 "^26.0.1" + jest-environment-jsdom "^26.3.0" + jest-environment-node "^26.3.0" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.4.2" jest-regex-util "^26.0.0" - jest-resolve "^26.0.1" - jest-util "^26.0.1" - jest-validate "^26.0.1" + jest-resolve "^26.4.0" + jest-util "^26.3.0" + jest-validate "^26.4.2" micromatch "^4.0.2" - pretty-format "^26.0.1" + pretty-format "^26.4.2" jest-css-modules@^2.1.0: version "2.1.0" @@ -12373,15 +13418,15 @@ jest-diff@^25.1.0, jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.5.0" -jest-diff@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.0.1.tgz#c44ab3cdd5977d466de69c46929e0e57f89aa1de" - integrity sha512-odTcHyl5X+U+QsczJmOjWw5tPvww+y9Yim5xzqxVl/R1j4z71+fHW4g8qu1ugMmKdFdxw+AtQgs5mupPnzcIBQ== +jest-diff@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.4.2.tgz#a1b7b303bcc534aabdb3bd4a7caf594ac059f5aa" + integrity sha512-6T1XQY8U28WH0Z5rGpQ+VqZSZz8EN8rZcBtfvXaOkbwxIEeRre6qnuZQlbY1AJ4MKDxQF8EkrCvK+hL/VkyYLQ== dependencies: chalk "^4.0.0" - diff-sequences "^26.0.0" - jest-get-type "^26.0.0" - pretty-format "^26.0.1" + diff-sequences "^26.3.0" + jest-get-type "^26.3.0" + pretty-format "^26.4.2" jest-docblock@^26.0.0: version "26.0.0" @@ -12390,39 +13435,41 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-each/-/jest-each-26.0.1.tgz#633083061619302fc90dd8f58350f9d77d67be04" - integrity sha512-OTgJlwXCAR8NIWaXFL5DBbeS4QIYPuNASkzSwMCJO+ywo9BEa6TqkaSWsfR7VdbMLdgYJqSfQcIyjJCNwl5n4Q== +jest-each@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-26.4.2.tgz#bb14f7f4304f2bb2e2b81f783f989449b8b6ffae" + integrity sha512-p15rt8r8cUcRY0Mvo1fpkOGYm7iI8S6ySxgIdfh3oOIv+gHwrHTy5VWCGOecWUhDsit4Nz8avJWdT07WLpbwDA== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" chalk "^4.0.0" - jest-get-type "^26.0.0" - jest-util "^26.0.1" - pretty-format "^26.0.1" + jest-get-type "^26.3.0" + jest-util "^26.3.0" + pretty-format "^26.4.2" -jest-environment-jsdom@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.0.1.tgz#217690852e5bdd7c846a4e3b50c8ffd441dfd249" - integrity sha512-u88NJa3aptz2Xix2pFhihRBAatwZHWwSiRLBDBQE1cdJvDjPvv7ZGA0NQBxWwDDn7D0g1uHqxM8aGgfA9Bx49g== +jest-environment-jsdom@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.3.0.tgz#3b749ba0f3a78e92ba2c9ce519e16e5dd515220c" + integrity sha512-zra8He2btIMJkAzvLaiZ9QwEPGEetbxqmjEBQwhH3CA+Hhhu0jSiEJxnJMbX28TGUvPLxBt/zyaTLrOPF4yMJA== dependencies: - "@jest/environment" "^26.0.1" - "@jest/fake-timers" "^26.0.1" - "@jest/types" "^26.0.1" - jest-mock "^26.0.1" - jest-util "^26.0.1" + "@jest/environment" "^26.3.0" + "@jest/fake-timers" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" + jest-mock "^26.3.0" + jest-util "^26.3.0" jsdom "^16.2.2" -jest-environment-node@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.0.1.tgz#584a9ff623124ff6eeb49e0131b5f7612b310b13" - integrity sha512-4FRBWcSn5yVo0KtNav7+5NH5Z/tEgDLp7VRQVS5tCouWORxj+nI+1tOLutM07Zb2Qi7ja+HEDoOUkjBSWZg/IQ== +jest-environment-node@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.3.0.tgz#56c6cfb506d1597f94ee8d717072bda7228df849" + integrity sha512-c9BvYoo+FGcMj5FunbBgtBnbR5qk3uky8PKyRVpSfe2/8+LrNQMiXX53z6q2kY+j15SkjQCOSL/6LHnCPLVHNw== dependencies: - "@jest/environment" "^26.0.1" - "@jest/fake-timers" "^26.0.1" - "@jest/types" "^26.0.1" - jest-mock "^26.0.1" - jest-util "^26.0.1" + "@jest/environment" "^26.3.0" + "@jest/fake-timers" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" + jest-mock "^26.3.0" + jest-util "^26.3.0" jest-esm-transformer@^1.0.0: version "1.0.0" @@ -12440,71 +13487,68 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" - integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== - -jest-get-type@^25.2.6: +jest-get-type@^25.1.0, jest-get-type@^25.2.6: version "25.2.6" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== -jest-get-type@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039" - integrity sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg== +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.0.1.tgz#40dcc03c43ac94d25b8618075804d09cd5d49de7" - integrity sha512-J9kBl/EdjmDsvyv7CiyKY5+DsTvVOScenprz/fGqfLg/pm1gdjbwwQ98nW0t+OIt+f+5nAVaElvn/6wP5KO7KA== +jest-haste-map@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.3.0.tgz#c51a3b40100d53ab777bfdad382d2e7a00e5c726" + integrity sha512-DHWBpTJgJhLLGwE5Z1ZaqLTYqeODQIZpby0zMBsCU9iRFHYyhklYqP4EiG73j5dkbaAdSZhgB938mL51Q5LeZA== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" "@types/graceful-fs" "^4.1.2" + "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" - jest-serializer "^26.0.0" - jest-util "^26.0.1" - jest-worker "^26.0.0" + jest-regex-util "^26.0.0" + jest-serializer "^26.3.0" + jest-util "^26.3.0" + jest-worker "^26.3.0" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" - which "^2.0.2" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.0.1.tgz#947c40ee816636ba23112af3206d6fa7b23c1c1c" - integrity sha512-ILaRyiWxiXOJ+RWTKupzQWwnPaeXPIoLS5uW41h18varJzd9/7I0QJGqg69fhTT1ev9JpSSo9QtalriUN0oqOg== +jest-jasmine2@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.4.2.tgz#18a9d5bec30904267ac5e9797570932aec1e2257" + integrity sha512-z7H4EpCldHN1J8fNgsja58QftxBSL+JcwZmaXIvV9WKIM+x49F4GLHu/+BQh2kzRKHAgaN/E82od+8rTOBPyPA== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.0.1" - "@jest/source-map" "^26.0.0" - "@jest/test-result" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/environment" "^26.3.0" + "@jest/source-map" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.0.1" + expect "^26.4.2" is-generator-fn "^2.0.0" - jest-each "^26.0.1" - jest-matcher-utils "^26.0.1" - jest-message-util "^26.0.1" - jest-runtime "^26.0.1" - jest-snapshot "^26.0.1" - jest-util "^26.0.1" - pretty-format "^26.0.1" + jest-each "^26.4.2" + jest-matcher-utils "^26.4.2" + jest-message-util "^26.3.0" + jest-runtime "^26.4.2" + jest-snapshot "^26.4.2" + jest-util "^26.3.0" + pretty-format "^26.4.2" throat "^5.0.0" -jest-leak-detector@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.0.1.tgz#79b19ab3f41170e0a78eb8fa754a116d3447fb8c" - integrity sha512-93FR8tJhaYIWrWsbmVN1pQ9ZNlbgRpfvrnw5LmgLRX0ckOJ8ut/I35CL7awi2ecq6Ca4lL59bEK9hr7nqoHWPA== +jest-leak-detector@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.4.2.tgz#c73e2fa8757bf905f6f66fb9e0070b70fa0f573f" + integrity sha512-akzGcxwxtE+9ZJZRW+M2o+nTNnmQZxrHJxX/HjgDaU5+PLmY1qnQPnMjgADPGCRPhB+Yawe1iij0REe+k/aHoA== dependencies: - jest-get-type "^26.0.0" - pretty-format "^26.0.1" + jest-get-type "^26.3.0" + pretty-format "^26.4.2" jest-matcher-utils@^25.1.0: version "25.1.0" @@ -12516,23 +13560,23 @@ jest-matcher-utils@^25.1.0: jest-get-type "^25.1.0" pretty-format "^25.1.0" -jest-matcher-utils@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.0.1.tgz#12e1fc386fe4f14678f4cc8dbd5ba75a58092911" - integrity sha512-PUMlsLth0Azen8Q2WFTwnSkGh2JZ8FYuwijC8NR47vXKpsrKmA1wWvgcj1CquuVfcYiDEdj985u5Wmg7COEARw== +jest-matcher-utils@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.4.2.tgz#fa81f3693f7cb67e5fc1537317525ef3b85f4b06" + integrity sha512-KcbNqWfWUG24R7tu9WcAOKKdiXiXCbMvQYT6iodZ9k1f7065k0keUOW6XpJMMvah+hTfqkhJhRXmA3r3zMAg0Q== dependencies: chalk "^4.0.0" - jest-diff "^26.0.1" - jest-get-type "^26.0.0" - pretty-format "^26.0.1" + jest-diff "^26.4.2" + jest-get-type "^26.3.0" + pretty-format "^26.4.2" -jest-message-util@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.0.1.tgz#07af1b42fc450b4cc8e90e4c9cef11b33ce9b0ac" - integrity sha512-CbK8uQREZ8umUfo8+zgIfEt+W7HAHjQCoRaNs4WxKGhAYBGwEyvxuK81FXa7VeB9pwDEXeeKOB2qcsNVCAvB7Q== +jest-message-util@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.3.0.tgz#3bdb538af27bb417f2d4d16557606fd082d5841a" + integrity sha512-xIavRYqr4/otGOiLxLZGj3ieMmjcNE73Ui+LdSW/Y790j5acqCsAdDiLIbzHCZMpN07JOENRWX5DcU+OQ+TjTA== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" "@types/stack-utils" "^1.0.1" chalk "^4.0.0" graceful-fs "^4.2.4" @@ -12540,164 +13584,169 @@ jest-message-util@^26.0.1: slash "^3.0.0" stack-utils "^2.0.2" -jest-mock@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-26.0.1.tgz#7fd1517ed4955397cf1620a771dc2d61fad8fd40" - integrity sha512-MpYTBqycuPYSY6xKJognV7Ja46/TeRbAZept987Zp+tuJvMN0YBWyyhG9mXyYQaU3SBI0TUlSaO5L3p49agw7Q== +jest-mock@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-26.3.0.tgz#ee62207c3c5ebe5f35b760e1267fee19a1cfdeba" + integrity sha512-PeaRrg8Dc6mnS35gOo/CbZovoDPKAeB1FICZiuagAgGvbWdNNyjQjkOaGUa/3N3JtpQ/Mh9P4A2D4Fv51NnP8Q== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" + "@types/node" "*" -jest-pnp-resolver@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" - integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== jest-regex-util@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.0.1.tgz#607ba7ccc32151d185a477cff45bf33bce417f0b" - integrity sha512-9d5/RS/ft0vB/qy7jct/qAhzJsr6fRQJyGAFigK3XD4hf9kIbEH5gks4t4Z7kyMRhowU6HWm/o8ILqhaHdSqLw== +jest-resolve-dependencies@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.4.2.tgz#739bdb027c14befb2fe5aabbd03f7bab355f1dc5" + integrity sha512-ADHaOwqEcVc71uTfySzSowA/RdxUpCxhxa2FNLiin9vWLB1uLPad3we+JSSROq5+SrL9iYPdZZF8bdKM7XABTQ== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" jest-regex-util "^26.0.0" - jest-snapshot "^26.0.1" + jest-snapshot "^26.4.2" -jest-resolve@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.0.1.tgz#21d1ee06f9ea270a343a8893051aeed940cde736" - integrity sha512-6jWxk0IKZkPIVTvq6s72RH735P8f9eCJW3IM5CX/SJFeKq1p2cZx0U49wf/SdMlhaB/anann5J2nCJj6HrbezQ== +jest-resolve@^26.4.0: + version "26.4.0" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.4.0.tgz#6dc0af7fb93e65b73fec0368ca2b76f3eb59a6d7" + integrity sha512-bn/JoZTEXRSlEx3+SfgZcJAVuTMOksYq9xe9O6s4Ekg84aKBObEaVXKOEilULRqviSLAYJldnoWV9c07kwtiCg== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" chalk "^4.0.0" graceful-fs "^4.2.4" - jest-pnp-resolver "^1.2.1" - jest-util "^26.0.1" + jest-pnp-resolver "^1.2.2" + jest-util "^26.3.0" read-pkg-up "^7.0.1" resolve "^1.17.0" slash "^3.0.0" -jest-runner@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-26.0.1.tgz#ea03584b7ae4bacfb7e533d680a575a49ae35d50" - integrity sha512-CApm0g81b49Znm4cZekYQK67zY7kkB4umOlI2Dx5CwKAzdgw75EN+ozBHRvxBzwo1ZLYZ07TFxkaPm+1t4d8jA== +jest-runner@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-26.4.2.tgz#c3ec5482c8edd31973bd3935df5a449a45b5b853" + integrity sha512-FgjDHeVknDjw1gRAYaoUoShe1K3XUuFMkIaXbdhEys+1O4bEJS8Avmn4lBwoMfL8O5oFTdWYKcf3tEJyyYyk8g== dependencies: - "@jest/console" "^26.0.1" - "@jest/environment" "^26.0.1" - "@jest/test-result" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/console" "^26.3.0" + "@jest/environment" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" + emittery "^0.7.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.0.1" + jest-config "^26.4.2" jest-docblock "^26.0.0" - jest-haste-map "^26.0.1" - jest-jasmine2 "^26.0.1" - jest-leak-detector "^26.0.1" - jest-message-util "^26.0.1" - jest-resolve "^26.0.1" - jest-runtime "^26.0.1" - jest-util "^26.0.1" - jest-worker "^26.0.0" + jest-haste-map "^26.3.0" + jest-leak-detector "^26.4.2" + jest-message-util "^26.3.0" + jest-resolve "^26.4.0" + jest-runtime "^26.4.2" + jest-util "^26.3.0" + jest-worker "^26.3.0" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.0.1.tgz#a121a6321235987d294168e282d52b364d7d3f89" - integrity sha512-Ci2QhYFmANg5qaXWf78T2Pfo6GtmIBn2rRaLnklRyEucmPccmCKvS9JPljcmtVamsdMmkyNkVFb9pBTD6si9Lw== +jest-runtime@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.4.2.tgz#94ce17890353c92e4206580c73a8f0c024c33c42" + integrity sha512-4Pe7Uk5a80FnbHwSOk7ojNCJvz3Ks2CNQWT5Z7MJo4tX0jb3V/LThKvD9tKPNVNyeMH98J/nzGlcwc00R2dSHQ== dependencies: - "@jest/console" "^26.0.1" - "@jest/environment" "^26.0.1" - "@jest/fake-timers" "^26.0.1" - "@jest/globals" "^26.0.1" - "@jest/source-map" "^26.0.0" - "@jest/test-result" "^26.0.1" - "@jest/transform" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/console" "^26.3.0" + "@jest/environment" "^26.3.0" + "@jest/fake-timers" "^26.3.0" + "@jest/globals" "^26.4.2" + "@jest/source-map" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" "@types/yargs" "^15.0.0" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.0.1" - jest-haste-map "^26.0.1" - jest-message-util "^26.0.1" - jest-mock "^26.0.1" + jest-config "^26.4.2" + jest-haste-map "^26.3.0" + jest-message-util "^26.3.0" + jest-mock "^26.3.0" jest-regex-util "^26.0.0" - jest-resolve "^26.0.1" - jest-snapshot "^26.0.1" - jest-util "^26.0.1" - jest-validate "^26.0.1" + jest-resolve "^26.4.0" + jest-snapshot "^26.4.2" + jest-util "^26.3.0" + jest-validate "^26.4.2" slash "^3.0.0" strip-bom "^4.0.0" yargs "^15.3.1" -jest-serializer@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.0.0.tgz#f6c521ddb976943b93e662c0d4d79245abec72a3" - integrity sha512-sQGXLdEGWFAE4wIJ2ZaIDb+ikETlUirEOBsLXdoBbeLhTHkZUJwgk3+M8eyFizhM6le43PDCCKPA1hzkSDo4cQ== +jest-serializer@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.3.0.tgz#1c9d5e1b74d6e5f7e7f9627080fa205d976c33ef" + integrity sha512-IDRBQBLPlKa4flg77fqg0n/pH87tcRKwe8zxOVTWISxGpPHYkRZ1dXKyh04JOja7gppc60+soKVZ791mruVdow== dependencies: + "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.0.1.tgz#1baa942bd83d47b837a84af7fcf5fd4a236da399" - integrity sha512-jxd+cF7+LL+a80qh6TAnTLUZHyQoWwEHSUFJjkw35u3Gx+BZUNuXhYvDqHXr62UQPnWo2P6fvQlLjsU93UKyxA== +jest-snapshot@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.4.2.tgz#87d3ac2f2bd87ea8003602fbebd8fcb9e94104f6" + integrity sha512-N6Uub8FccKlf5SBFnL2Ri/xofbaA68Cc3MGjP/NuwgnsvWh+9hLIR/DhrxbSiKXMY9vUW5dI6EW1eHaDHqe9sg== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" "@types/prettier" "^2.0.0" chalk "^4.0.0" - expect "^26.0.1" + expect "^26.4.2" graceful-fs "^4.2.4" - jest-diff "^26.0.1" - jest-get-type "^26.0.0" - jest-matcher-utils "^26.0.1" - jest-message-util "^26.0.1" - jest-resolve "^26.0.1" - make-dir "^3.0.0" + jest-diff "^26.4.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.3.0" + jest-matcher-utils "^26.4.2" + jest-message-util "^26.3.0" + jest-resolve "^26.4.0" natural-compare "^1.4.0" - pretty-format "^26.0.1" + pretty-format "^26.4.2" semver "^7.3.2" -jest-util@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.0.1.tgz#72c4c51177b695fdd795ca072a6f94e3d7cef00a" - integrity sha512-byQ3n7ad1BO/WyFkYvlWQHTsomB6GIewBh8tlGtusiylAlaxQ1UpS0XYH0ngOyhZuHVLN79Qvl6/pMiDMSSG1g== +jest-util@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz#a8974b191df30e2bf523ebbfdbaeb8efca535b3e" + integrity sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" graceful-fs "^4.2.4" is-ci "^2.0.0" - make-dir "^3.0.0" + micromatch "^4.0.2" -jest-validate@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-26.0.1.tgz#a62987e1da5b7f724130f904725e22f4e5b2e23c" - integrity sha512-u0xRc+rbmov/VqXnX3DlkxD74rHI/CfS5xaV2VpeaVySjbb1JioNVOyly5b56q2l9ZKe7bVG5qWmjfctkQb0bA== +jest-validate@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-26.4.2.tgz#e871b0dfe97747133014dcf6445ee8018398f39c" + integrity sha512-blft+xDX7XXghfhY0mrsBCYhX365n8K5wNDC4XAcNKqqjEzsRUSXP44m6PL0QJEW2crxQFLLztVnJ4j7oPlQrQ== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" camelcase "^6.0.0" chalk "^4.0.0" - jest-get-type "^26.0.0" + jest-get-type "^26.3.0" leven "^3.1.0" - pretty-format "^26.0.1" + pretty-format "^26.4.2" -jest-watcher@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.0.1.tgz#5b5e3ebbdf10c240e22a98af66d645631afda770" - integrity sha512-pdZPydsS8475f89kGswaNsN3rhP6lnC3/QDCppP7bg1L9JQz7oU9Mb/5xPETk1RHDCWeqmVC47M4K5RR7ejxFw== +jest-watcher@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.3.0.tgz#f8ef3068ddb8af160ef868400318dc4a898eed08" + integrity sha512-XnLdKmyCGJ3VoF6G/p5ohbJ04q/vv5aH9ENI+i6BL0uu9WWB6Z7Z2lhQQk0d2AVZcRGp1yW+/TsoToMhBFPRdQ== dependencies: - "@jest/test-result" "^26.0.1" - "@jest/types" "^26.0.1" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.0.1" + jest-util "^26.3.0" string-length "^4.0.1" jest-worker@^25.1.0: @@ -12708,22 +13757,23 @@ jest-worker@^25.1.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz#4920c7714f0a96c6412464718d0c58a3df3fb066" - integrity sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw== +jest-worker@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" + integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== dependencies: + "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" jest@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/jest/-/jest-26.0.1.tgz#5c51a2e58dff7525b65f169721767173bf832694" - integrity sha512-29Q54kn5Bm7ZGKIuH2JRmnKl85YRigp0o0asTc6Sb6l2ch1DCXIeZTLLFy9ultJvhkTqbswF5DEx4+RlkmCxWg== + version "26.4.2" + resolved "https://registry.npmjs.org/jest/-/jest-26.4.2.tgz#7e8bfb348ec33f5459adeaffc1a25d5752d9d312" + integrity sha512-LLCjPrUh98Ik8CzW8LLVnSCfLaiY+wbK53U7VxnFSX7Q+kWC4noVeDvGWIFw0Amfq1lq2VfGm7YHWSLBV62MJw== dependencies: - "@jest/core" "^26.0.1" + "@jest/core" "^26.4.2" import-local "^3.0.2" - jest-cli "^26.0.1" + jest-cli "^26.4.2" jose@^1.27.1: version "1.27.1" @@ -12732,11 +13782,25 @@ jose@^1.27.1: dependencies: "@panva/asn1.js" "^1.0.0" +jpegtran-bin@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz#d00aed809fba7aa6f30817e59eee4ddf198f8f10" + integrity sha512-2cRl1ism+wJUoYAYFt6O/rLBfpXNWG2dUWbgcEkTt5WGMnqI46eEro8T4C5zGROxKRqyKpCBSdHPvt5UYCtxaQ== + dependencies: + bin-build "^3.0.0" + bin-wrapper "^4.0.0" + logalot "^2.0.0" + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== +js-file-download@^0.4.1: + version "0.4.12" + resolved "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821" + integrity sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg== + js-string-escape@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -12752,10 +13816,10 @@ js-tokens@^3.0.2: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.13.1, js-yaml@^3.8.3: - version "3.13.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== +js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1, js-yaml@^3.8.3: + version "3.14.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -12867,11 +13931,23 @@ json-buffer@3.0.0: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-pointer@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz#8e500550a6aac5464a473377da57aa6cc22828d7" + integrity sha1-jlAFUKaqxUZKRzN32leqbMIoKNc= + dependencies: + foreach "^2.0.4" + json-schema-compare@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56" @@ -12888,6 +13964,25 @@ json-schema-merge-allof@^0.6.0: json-schema-compare "^0.2.2" lodash "^4.17.4" +json-schema-migrate@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-0.2.0.tgz#ba47a5b0072fc72396460e1bd60b44d52178bbc6" + integrity sha1-ukelsAcvxyOWRg4b1gtE1SF4u8Y= + dependencies: + ajv "^5.0.0" + +json-schema-ref-parser@^9.0.6: + version "9.0.6" + resolved "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#fc89a5e6b853f2abe8c0af30d3874196526adb60" + integrity sha512-z0JGv7rRD3CnJbZY/qCpscyArdtLJhr/wRBmFUdoZ8xMjsFyNdILSprG2degqRLjBjyhZHAEBpGOxniO9rKTxA== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.6" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -12913,6 +14008,14 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json-to-ast@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz#041a9fcd03c0845036acb670d29f425cea4faaf9" + integrity sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ== + dependencies: + code-error-fragment "0.0.230" + grapheme-splitter "^1.0.4" + json3@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" @@ -13074,6 +14177,13 @@ jwt-decode@2.2.0: resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk= +keyv@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" + integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== + dependencies: + json-buffer "3.0.0" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -13081,6 +14191,13 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz#9fe703cb4a94d6d11729d320af033307efd02ee6" + integrity sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw== + dependencies: + json-buffer "3.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -13130,25 +14247,25 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knex@^0.21.1: - version "0.21.1" - resolved "https://registry.npmjs.org/knex/-/knex-0.21.1.tgz#4fba7e6c58c9f459846c3090be157a732fc75e41" - integrity sha512-uWszXC2DPaLn/YznGT9wFTWUG9+kqbL4DMz+hCH789GLcLuYzq8werHPDKBJxtKvxrW/S1XIXgrTWdMypiVvsw== + version "0.21.5" + resolved "https://registry.npmjs.org/knex/-/knex-0.21.5.tgz#c4be1958488f348aed3510aa4b7115639ee1bd01" + integrity sha512-cQj7F2D/fu03eTr6ZzYCYKdB9w7fPYlvTiU/f2OeXay52Pq5PwD+NAkcf40WDnppt/4/4KukROwlMOaE7WArcA== dependencies: - colorette "1.1.0" + colorette "1.2.1" commander "^5.1.0" debug "4.1.1" esm "^3.2.25" getopts "2.2.5" inherits "~2.0.4" - interpret "^2.0.0" + interpret "^2.2.0" liftoff "3.1.0" - lodash "^4.17.15" + lodash "^4.17.20" mkdirp "^1.0.4" - pg-connection-string "2.2.0" + pg-connection-string "2.3.0" tarn "^3.0.0" tildify "2.0.0" uuid "^7.0.3" - v8flags "^3.1.3" + v8flags "^3.2.0" kuler@1.0.x: version "1.0.1" @@ -13164,7 +14281,7 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" -lazy-ass@1.6.0: +lazy-ass@1.6.0, lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= @@ -13179,6 +14296,13 @@ lazy-cache@^1.0.3: resolved "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= +lazy-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ= + dependencies: + set-getter "^0.1.0" + lazy-universal-dotenv@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-3.0.1.tgz#a6c8938414bca426ab8c9463940da451a911db38" @@ -13301,6 +14425,16 @@ lint-staged@^10.1.0: string-argv "0.3.1" stringify-object "^3.3.0" +list-item@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz#0c65d00e287cb663ccb3cb3849a77e89ec268a56" + integrity sha1-DGXQDih8tmPMs8s4Sad+iewmilY= + dependencies: + expand-range "^1.8.1" + extend-shallow "^2.0.1" + is-number "^2.1.0" + repeat-string "^1.5.2" + listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" @@ -13344,7 +14478,7 @@ listr2@^2.1.0: rxjs "^6.5.5" through "^2.3.8" -listr@0.14.3: +listr@^0.14.3: version "0.14.3" resolved "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -13359,6 +14493,11 @@ listr@0.14.3: p-map "^2.0.0" rxjs "^6.3.3" +livereload-js@^2.3.0: + version "2.4.0" + resolved "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" + integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -13471,22 +14610,47 @@ lodash.assign@^4.1.0, lodash.assign@^4.2.0: resolved "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= +lodash.assignin@^4.0.9: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" + integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= + +lodash.bind@^4.1.4: + version "4.2.1" + resolved "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + integrity sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw= + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.debounce@^4.0.8: +lodash.debounce@^4, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.flatten@^4.4.0: +lodash.defaults@^4.0.1: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.filter@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" + integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= + +lodash.flatten@^4.2.0, lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= @@ -13496,6 +14660,11 @@ lodash.flattendeep@^4.0.0: resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.foreach@^4.3.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -13506,27 +14675,62 @@ lodash.ismatch@^4.4.0: resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.map@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= + lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.merge@^4.4.0: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.padstart@^4.6.1: + version "4.6.1" + resolved "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + integrity sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs= + +lodash.pick@^4.2.1: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + +lodash.reduce@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" + integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= + +lodash.reject@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" + integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU= + lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= +lodash.some@^4.4.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" + integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash.template@^4.0.2, lodash.template@^4.5.0: +lodash.template@^4.0.2, lodash.template@^4.4.0, lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -13556,22 +14760,10 @@ lodash.without@^4.4.0: resolved "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= -lodash@4.17.15: - version "4.17.15" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.2.1: - version "4.17.19" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -log-symbols@3.0.0, log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10: + version "4.17.20" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== log-symbols@^1.0.2: version "1.0.2" @@ -13580,6 +14772,13 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + log-symbols@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" @@ -13606,6 +14805,14 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +logalot@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz#5f8e8c90d304edf12530951a5554abb8c5e3f552" + integrity sha1-X46MkNME7fElMJUaVVSruMXj9VI= + dependencies: + figures "^1.3.5" + squeak "^1.0.0" + logform@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360" @@ -13627,6 +14834,11 @@ long@^4.0.0: resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +longest@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -13649,6 +14861,11 @@ lower-case@^2.0.1: dependencies: tslib "^1.10.0" +lowercase-keys@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -13659,6 +14876,14 @@ lowercase-keys@^2.0.0: resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lowlight@1.12.1: + version "1.12.1" + resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb" + integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w== + dependencies: + fault "^1.0.2" + highlight.js "~9.15.0" + lowlight@^1.14.0: version "1.14.0" resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.14.0.tgz#83ebc143fec0f9e6c0d3deffe01be129ce56b108" @@ -13675,6 +14900,24 @@ lowlight@~1.11.0: fault "^1.0.2" highlight.js "~9.13.0" +lpad-align@^1.0.1: + version "1.1.2" + resolved "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz#21f600ac1c3095c3c6e497ee67271ee08481fe9e" + integrity sha1-IfYArBwwlcPG5JfuZyce4ISB/p4= + dependencies: + get-stdin "^4.0.1" + indent-string "^2.1.0" + longest "^1.0.0" + meow "^3.3.0" + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + lru-cache@^5.0.0, lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -13682,6 +14925,13 @@ lru-cache@^5.0.0, lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-queue@0.1: + version "0.1.0" + resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= + dependencies: + es5-ext "~0.10.2" + macos-release@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" @@ -13694,7 +14944,7 @@ magic-string@^0.25.2: dependencies: sourcemap-codec "^1.4.4" -make-dir@^1.0.0: +make-dir@^1.0.0, make-dir@^1.2.0: version "1.3.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== @@ -13805,6 +15055,22 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-it@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-9.1.0.tgz#df9601c168568704d554b1fff9af0c5b561168d9" + integrity sha512-xHKG4C8iPriyfu/jc2hsCC045fKrMQ0VexX2F1FGYiRxDxqMB2aAhF8WauJ3fltn2kb90moGBkiiEdooGIg55w== + dependencies: + argparse "^1.0.7" + entities "~1.1.1" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +markdown-link@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz#32c5c65199a6457316322d1e4229d13407c8c7cf" + integrity sha1-MsXGUZmmRXMWMi0eQinRNAfIx88= + markdown-to-jsx@^6.11.4: version "6.11.4" resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.4.tgz#b4528b1ab668aef7fe61c1535c27e837819392c5" @@ -13813,7 +15079,25 @@ markdown-to-jsx@^6.11.4: prop-types "^15.6.2" unquote "^1.1.0" -material-table@1.68.x: +markdown-toc@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz#44a15606844490314afc0444483f9e7b1122c339" + integrity sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg== + dependencies: + concat-stream "^1.5.2" + diacritics-map "^0.1.0" + gray-matter "^2.1.0" + lazy-cache "^2.0.2" + list-item "^1.1.1" + markdown-link "^0.1.1" + minimist "^1.2.0" + mixin-deep "^1.1.3" + object.pick "^1.2.0" + remarkable "^1.7.1" + repeat-string "^1.6.1" + strip-color "^0.1.0" + +material-table@1.68.0: version "1.68.0" resolved "https://registry.npmjs.org/material-table/-/material-table-1.68.0.tgz#275c3d9a885c40ae4bc5a7461c00e877f92397b9" integrity sha512-dyJJaVsS3m+i6sn71AvYcVdA1P9X1XiUOM2PekfvEeeMtkdQb66oChGkk77ndYi3Ja6j4DovGVNrgeVLwXLZiw== @@ -13831,6 +15115,11 @@ material-table@1.68.x: react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" +math-random@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -13872,6 +15161,20 @@ memoize-one@^5.1.1: resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== +memoizee@^0.4.12: + version "0.4.14" + resolved "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" + integrity sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg== + dependencies: + d "1" + es5-ext "^0.10.45" + es6-weak-map "^2.0.2" + event-emitter "^0.3.5" + is-promise "^2.1" + lru-queue "0.1" + next-tick "1" + timers-ext "^0.1.5" + memoizerific@^1.11.3: version "1.11.3" resolved "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" @@ -13965,6 +15268,11 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== +merge@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" + integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== + methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -14015,12 +15323,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": - version "1.43.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== - -mime-db@1.44.0: +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: version "1.44.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== @@ -14037,14 +15340,7 @@ mime-types@2.1.18: dependencies: mime-db "~1.33.0" -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== - dependencies: - mime-db "1.43.0" - -mime-types@^2.1.26: +mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -14076,6 +15372,16 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0, mimic-response@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -14118,7 +15424,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -14133,7 +15439,7 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" -minimist@1.2.5, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -14210,7 +15516,7 @@ mitt@^1.1.2: resolved "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== -mixin-deep@^1.2.0: +mixin-deep@^1.1.3, mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== @@ -14243,7 +15549,7 @@ mkdirp@*, mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -14255,12 +15561,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@2.26.0, moment@^2.25.3, moment@^2.26.0: - version "2.26.0" - resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" - integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== - -moment@^2.27.0: +moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: version "2.27.0" resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== @@ -14317,7 +15618,7 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -msw@^0.19.0, msw@^0.19.5: +msw@^0.19.5: version "0.19.5" resolved "https://registry.npmjs.org/msw/-/msw-0.19.5.tgz#7d2a1a852ccf1644d3db6735d69fff6777aac33f" integrity sha512-J5eQ++gDVZoHPC8gVXtWcakLjgmPipvFj/sEnlRV/WViXuiq2CamSqO3Wbh6H8bAmj+k2vUWCfcVT1HjMdKB2Q== @@ -14333,6 +15634,23 @@ msw@^0.19.0, msw@^0.19.5: statuses "^2.0.0" yargs "^15.3.1" +msw@^0.20.5: + version "0.20.5" + resolved "https://registry.npmjs.org/msw/-/msw-0.20.5.tgz#b6141080c0d8b17c451d9ca36c28cc47b4ac487a" + integrity sha512-hmEsey5BbVicMGt7aOh/GZ9ltga5N3tK6NiJXnbfCkAGKgnAVnjASr3i7Z+sWlZyY5uuMUFyLCEcqrlXxC6qIA== + dependencies: + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.0" + chalk "^4.1.0" + cookie "^0.4.1" + graphql "^15.3.0" + headers-utils "^1.2.0" + node-fetch "^2.6.0" + node-match-path "^0.4.4" + node-request-interceptor "^0.3.5" + statuses "^2.0.0" + yargs "^15.4.1" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -14375,17 +15693,12 @@ mz@^2.5.0, mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.12.1: - version "2.14.0" - resolved "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -nan@^2.14.0: +nan@^2.12.1, nan@^2.14.0: version "2.14.1" resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== -nano-css@^5.2.1: +nano-css@^5.1.0, nano-css@^5.2.1: version "5.3.0" resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.3.0.tgz#9d3cd29788d48b6a07f52aa4aec7cf4da427b6b5" integrity sha512-uM/9NGK9/E9/sTpbIZ/bQ9xOLOIHZwrrb/CRlbDHBU/GFS7Gshl24v/WJhwsVViWkpOXUmiZ66XO7fSB4Wd92Q== @@ -14445,6 +15758,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== +next-tick@1: + version "1.1.0" + resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + next-tick@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -14562,26 +15880,26 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-match-path@^0.4.2: - version "0.4.2" - resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.2.tgz#30cc39510fa493bff03c3d0d2fff711c868ec457" - integrity sha512-wfde4FOC5A8RTSUVZ7pTpBV+dJsr2vVxT6374VrNam6wnnhx6EvwAwL/E/r3AW/YU6XkeZggF5xfBlu4a/ULBg== +node-match-path@^0.4.2, node-match-path@^0.4.4: + version "0.4.4" + resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.4.tgz#516a10926093c0cc6f237d020685b593b19baebb" + integrity sha512-pBq9gp7TG0r0VXuy/oeZmQsjBSnYQo7G886Ly/B3azRwZuEtHCY155dzmfoKWcDPGgyfIGD8WKVC7h3+6y7yTg== node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-notifier@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/node-notifier/-/node-notifier-7.0.0.tgz#513bc42f2aa3a49fce1980a7ff375957c71f718a" - integrity sha512-y8ThJESxsHcak81PGpzWwQKxzk+5YtP3IxR8AYdpXQ1IB6FmcVzFdZXrkPin49F/DKUCfeeiziB8ptY9npzGuA== +node-notifier@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620" + integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA== dependencies: growly "^1.3.0" - is-wsl "^2.1.1" - semver "^7.2.1" + is-wsl "^2.2.0" + semver "^7.3.2" shellwords "^0.1.1" - uuid "^7.0.3" + uuid "^8.3.0" which "^2.0.2" node-pre-gyp@^0.11.0: @@ -14616,14 +15934,7 @@ node-pre-gyp@^0.13.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.29, node-releases@^1.1.52: - version "1.1.52" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" - integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ== - dependencies: - semver "^6.3.0" - -node-releases@^1.1.58: +node-releases@^1.1.29, node-releases@^1.1.52, node-releases@^1.1.58: version "1.1.60" resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== @@ -14636,6 +15947,15 @@ node-request-interceptor@^0.2.5: debug "^4.1.1" headers-utils "^1.2.0" +node-request-interceptor@^0.3.5: + version "0.3.5" + resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.3.5.tgz#4b26159617829c9a70643012c0fdc3ae4c78ae43" + integrity sha512-lBwA3v00hxCW2xhhZHZ1ab5JMHoAnzgtbeaUXqTufRh7mpAG93ZOChj0btMDB1VZd+CKhCbtigsxCjmerKa2+w== + dependencies: + "@open-draft/until" "^1.0.3" + debug "^4.1.1" + headers-utils "^1.2.0" + nodegit-promise@~4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/nodegit-promise/-/nodegit-promise-4.0.0.tgz#5722b184f2df7327161064a791d2e842c9167b34" @@ -14659,6 +15979,21 @@ nodegit@0.26.5: request-promise-native "^1.0.5" tar-fs "^1.16.3" +nodegit@^0.27.0: + version "0.27.0" + resolved "https://registry.npmjs.org/nodegit/-/nodegit-0.27.0.tgz#4e8cc236f60e1c97324a5acff99056fe116a6ebe" + integrity sha512-E9K4gPjWiA0b3Tx5lfWCzG7Cvodi2idl3V5UD2fZrOrHikIfrN7Fc2kWLtMUqqomyoToYJLeIC8IV7xb1CYRLA== + dependencies: + fs-extra "^7.0.0" + got "^10.7.0" + json5 "^2.1.0" + lodash "^4.17.14" + nan "^2.14.0" + node-gyp "^4.0.0" + node-pre-gyp "^0.13.0" + ramda "^0.25.0" + tar-fs "^1.16.3" + nodemon@^2.0.2: version "2.0.4" resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" @@ -14734,6 +16069,15 @@ normalize-url@1.9.1: query-string "^4.1.0" sort-keys "^1.0.0" +normalize-url@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + normalize-url@^3.0.0, normalize-url@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" @@ -14751,6 +16095,14 @@ npm-bundled@^1.0.1: dependencies: npm-normalize-package-bin "^1.0.1" +npm-conf@^1.1.0: + version "1.1.3" + resolved "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" + integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== + dependencies: + config-chain "^1.1.11" + pify "^3.0.0" + npm-lifecycle@^3.1.2: version "3.1.4" resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.4.tgz#de6975c7d8df65f5150db110b57cce498b0b604c" @@ -14993,7 +16345,7 @@ omggif@1.0.7: resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d" integrity sha1-WdLuywJj3oRjWz/riHwMmXPx5J0= -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -15051,6 +16403,13 @@ open@^7.0.0, open@^7.0.2: is-docker "^2.0.0" is-wsl "^2.1.1" +openapi-sampler@^1.0.0-beta.15: + version "1.0.0-beta.16" + resolved "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.16.tgz#7813524d5b88d222efb772ceb5a809075d6d9174" + integrity sha512-05+GvwMagTY7GxoDQoWJfmAUFlxfebciiEzqKmu4iq6+MqBEn62AMUkn0CTxyKhnUGIaR2KXjTeslxIeJwVIOw== + dependencies: + json-pointer "^0.6.0" + opencollective-postinstall@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" @@ -15087,6 +16446,15 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +optipng-bin@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/optipng-bin/-/optipng-bin-5.1.0.tgz#a7c7ab600a3ab5a177dae2f94c2d800aa386b5a9" + integrity sha512-9baoqZTNNmXQjq/PQTWEXbVV3AMO2sI/GaaqZJZ8SExfAzjijeAP7FEeT+TtyumSw7gr0PZtSUYB/Ke7iHQVKA== + dependencies: + bin-build "^3.0.0" + bin-wrapper "^4.0.0" + logalot "^2.0.0" + ora@*, ora@^4.0.3: version "4.0.4" resolved "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" @@ -15113,6 +16481,13 @@ os-browserify@^0.3.0: resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +os-filter-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz#1c0b62d5f3a2442749a2d139e6dddee6e81d8d16" + integrity sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg== + dependencies: + arch "^2.1.0" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -15146,21 +16521,57 @@ osenv@0, osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -ospath@1.2.2: +ospath@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= +p-cancelable@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw== + +p-cancelable@^0.4.0: + version "0.4.1" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + p-each-series@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== +p-event@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz#8e6b4f4f65c72bc5b6fe28b75eda874f96a4a085" + integrity sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU= + dependencies: + p-timeout "^1.1.1" + +p-event@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" + integrity sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA== + dependencies: + p-timeout "^2.0.1" + +p-event@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" + integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== + dependencies: + p-timeout "^3.1.0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -15171,6 +16582,11 @@ p-finally@^2.0.0: resolved "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== +p-is-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= + p-limit@3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" @@ -15239,7 +16655,7 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-pipe@^1.2.0: +p-pipe@^1.1.0, p-pipe@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" integrity sha1-SxoROZoRUgpneQ7loMHViB1r7+k= @@ -15271,6 +16687,20 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" +p-timeout@^1.1.1: + version "1.2.1" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386" + integrity sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y= + dependencies: + p-finally "^1.0.0" + +p-timeout@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== + dependencies: + p-finally "^1.0.0" + p-timeout@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -15320,6 +16750,11 @@ pako@~1.0.5: resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +papi@^0.29.0: + version "0.29.1" + resolved "https://registry.npmjs.org/papi/-/papi-0.29.1.tgz#7373e2c527f5117d61fd2a0e6c6b1dd72bf7f180" + integrity sha512-Y9ipSMfWuuVFO3zY9PlxOmEg+bQ7CeJ28sa9/a0veYNynLf9fwjR3+3fld5otEy7okUaEOUuCHVH62MyTmACXQ== + parallel-transform@^1.1.0: version "1.2.0" resolved "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" @@ -15501,6 +16936,14 @@ passport-google-oauth20@^2.0.0: dependencies: passport-oauth2 "1.x.x" +passport-microsoft@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4" + integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw== + dependencies: + passport-oauth2 "1.2.0" + pkginfo "0.2.x" + passport-oauth1@1.x.x: version "1.1.0" resolved "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918" @@ -15510,6 +16953,15 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" +passport-oauth2@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2" + integrity sha1-SWE6PsqFx6HmW/EBnitrgKEMisI= + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" @@ -15695,6 +17147,11 @@ pend@~1.2.0: resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -15705,12 +17162,7 @@ pg-connection-string@0.1.3, pg-connection-string@^0.1.3: resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= -pg-connection-string@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.2.0.tgz#caab4d38a9de4fdc29c9317acceed752897de41c" - integrity sha512-xB/+wxcpFipUZOQcSzcgkjcNOosGhEoPSjz06jC89lv1dj7mc9bZv6wLVy8M2fVjP0a/xN0N988YDq1L0FhK3A== - -pg-connection-string@^2.3.0: +pg-connection-string@2.3.0, pg-connection-string@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6" integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w== @@ -15842,7 +17294,7 @@ pinkie@^2.0.0: resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pirates@^4.0.1: +pirates@^4.0.0, pirates@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== @@ -15908,26 +17360,26 @@ pnp-webpack-plugin@1.5.0: dependencies: ts-pnp "^1.1.2" -polished@^3.3.1: - version "3.5.0" - resolved "https://registry.npmjs.org/polished/-/polished-3.5.0.tgz#bffb0db79025c61d1f735f9bb77a379f5fc46345" - integrity sha512-TujkqjczBuuG8ObaNeq+zCCu46tTdaWxqtMxCGTxsV5NYTr9pL08H1P0jgy1V4PXLWm2UlIj+icSEwAZv7I8TA== +polished@^3.3.1, polished@^3.4.4: + version "3.6.5" + resolved "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz#dbefdde64c675935ec55119fe2a2ab627ca82e9c" + integrity sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ== dependencies: - "@babel/runtime" "^7.8.7" + "@babel/runtime" "^7.9.2" popper.js@^1.14.1, popper.js@^1.14.4, popper.js@^1.14.7: version "1.16.1" resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== -portfinder@^1.0.26: - version "1.0.26" - resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" - integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== +portfinder@^1.0.25, portfinder@^1.0.26: + version "1.0.28" + resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== dependencies: async "^2.6.2" debug "^3.1.1" - mkdirp "^0.5.1" + mkdirp "^0.5.5" posix-character-classes@^0.1.0: version "0.1.1" @@ -16309,7 +17761,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: +"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.23, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.32" resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -16373,7 +17825,7 @@ prelude-ls@~1.1.2: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-http@^1.0.0: +prepend-http@^1.0.0, prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= @@ -16393,7 +17845,7 @@ prettier@^2.0.5: resolved "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== -pretty-bytes@5.3.0: +pretty-bytes@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== @@ -16406,16 +17858,6 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^24.3.0: - version "24.9.0" - resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" - integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== - dependencies: - "@jest/types" "^24.9.0" - ansi-regex "^4.0.0" - ansi-styles "^3.2.0" - react-is "^16.8.4" - pretty-format@^25.1.0, pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" @@ -16426,12 +17868,12 @@ pretty-format@^25.1.0, pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.0.1: - version "26.0.1" - resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.0.1.tgz#a4fe54fe428ad2fd3413ca6bbd1ec8c2e277e197" - integrity sha512-SWxz6MbupT3ZSlL0Po4WF/KujhQaVehijR2blyRDCzk9e45EaYMVhMBn49fnRuHxtkSpXTes1GxNpVmH86Bxfw== +pretty-format@^26.4.2: + version "26.4.2" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" + integrity sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA== dependencies: - "@jest/types" "^26.0.1" + "@jest/types" "^26.3.0" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^16.12.0" @@ -16441,10 +17883,10 @@ pretty-hrtime@^1.0.3: resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prismjs@^1.20.0, prismjs@^1.8.4, prismjs@~1.20.0: - version "1.20.0" - resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" - integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== +prismjs@^1.17.1, prismjs@^1.21.0, prismjs@^1.8.4, prismjs@~1.21.0: + version "1.21.0" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" + integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== optionalDependencies: clipboard "^2.0.0" @@ -16475,6 +17917,13 @@ progress@^2.0.0: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@^12.0.0: + version "12.0.0" + resolved "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" + integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== + dependencies: + tdigest "^0.1.1" + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -16552,7 +18001,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -16610,6 +18059,11 @@ ps-tree@1.2.0: dependencies: event-stream "=3.3.4" +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + psl@^1.1.28: version "1.8.0" resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -16697,10 +18151,10 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.6.0: - version "6.9.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" - integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== +qs@^6.4.0, qs@^6.5.1, qs@^6.6.0, qs@^6.9.4: + version "6.9.4" + resolved "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== qs@~6.5.2: version "6.5.2" @@ -16715,6 +18169,20 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-browser@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/querystring-browser/-/querystring-browser-1.0.4.tgz#f2e35881840a819bc7b1bf597faf0979e6622dc6" + integrity sha1-8uNYgYQKgZvHsb9Zf68JeeZiLcY= + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -16740,23 +18208,23 @@ quick-lru@^4.0.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + raf-schd@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@^3.4.1: +raf@^3.1.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: performance-now "^2.1.0" -ramda@0.26.1, ramda@^0.26: - version "0.26.1" - resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== - ramda@^0.21.0: version "0.21.0" resolved "https://registry.npmjs.org/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" @@ -16767,7 +18235,31 @@ ramda@^0.25.0: resolved "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +ramda@^0.26, ramda@~0.26.1: + version "0.26.1" + resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + +ramldt2jsonschema@^1.1.0: + version "1.2.3" + resolved "https://registry.npmjs.org/ramldt2jsonschema/-/ramldt2jsonschema-1.2.3.tgz#8d45a7f306a1169a3bde91cab725d1d6d0ff7d55" + integrity sha512-+wLDAV2NNv9NkfEUOYStaDu/6RYgYXeC1zLtXE+dMU/jDfjpN4iJnBGycDwFTFaIQGosOQhxph7fEX6Mpwxdug== + dependencies: + commander "^5.0.0" + js-yaml "^3.14.0" + json-schema-migrate "^0.2.0" + webapi-parser "^0.5.0" + +randomatic@^3.0.0: + version "3.1.1" + resolved "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" + integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -16802,6 +18294,14 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@~1.1.0: + version "1.1.7" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425" + integrity sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU= + dependencies: + bytes "1" + string_decoder "0.10" + raw-loader@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz#5e9d399a5a222cc0de18f42c3bc5e49677532b3f" @@ -16867,6 +18367,22 @@ react-clientside-effect@^1.2.2: dependencies: "@babel/runtime" "^7.0.0" +react-copy-to-clipboard@5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e" + integrity sha512-ELKq31/E3zjFs5rDWNCfFL4NvNFQvGRoJdAKReD/rUPA+xxiLPQmZBZBvy2vgH7V0GE9isIQpT9WXbwIVErYdA== + dependencies: + copy-to-clipboard "^3" + prop-types "^15.5.8" + +react-debounce-input@^3.2.0: + version "3.2.2" + resolved "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.2.2.tgz#d2cc99c1ce47fae89037965f5699edc1b0317197" + integrity sha512-RIBu68Cq/gImKz/2h1cE042REDqyqj3D+7SJ3lnnIpJX0ht9D9PfH7KAnL+SgDz6hvKa9pZS2CnAxlkrLmnQlg== + dependencies: + lodash.debounce "^4" + prop-types "^15.7.2" + react-dev-utils@^10.2.1: version "10.2.1" resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz#f6de325ae25fa4d546d09df4bb1befdc6dd19c19" @@ -16897,7 +18413,7 @@ react-dev-utils@^10.2.1: strip-ansi "6.0.0" text-table "0.2.0" -react-dev-utils@^9.0.0: +react-dev-utils@^9.0.0, react-dev-utils@^9.1.0: version "9.1.0" resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz#3ad2bb8848a32319d760d0a84c56c14bdaae5e81" integrity sha512-X2KYF/lIGyGwP/F/oXgGDF24nxDA2KC4b7AFto+eqzc/t838gpSGiaU8trTqHXOohuLxxc5qi1eDzsl9ucPDpg== @@ -16942,7 +18458,7 @@ react-docgen@^5.0.0: node-dir "^0.1.10" strip-indent "^3.0.0" -react-dom@^16.12.0, react-dom@^16.13.1, react-dom@^16.8.3: +react-dom@^16.12.0, react-dom@^16.13.1, react-dom@^16.8.3, react-dom@^16.8.4: version "16.13.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== @@ -17013,10 +18529,10 @@ react-helmet@6.1.0: react-fast-compare "^3.1.1" react-side-effect "^2.1.0" -react-hook-form@^5.7.2: - version "5.7.2" - resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.7.2.tgz#a84e259e5d37dd30949af4f79c4dac31101b79ac" - integrity sha512-bJvY348vayIvEUmSK7Fvea/NgqbT2racA2IbnJz/aPlQ3GBtaTeDITH6rtCa6y++obZzG6E3Q8VuoXPir7QYUg== +react-hook-form@^6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.6.0.tgz#bda76fc50a916096afe18119f6f57f2b3911cdb7" + integrity sha512-5UKx2SBMaWuwy614H730Vv2QmoGdsdYpN96MnWXYzC8cHIUi9N2hYwtI86TEIcBHJ6FvEewh3onfA7P243RM1g== react-hot-loader@^4.12.21: version "4.12.21" @@ -17039,6 +18555,27 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" +react-immutable-proptypes@2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" + integrity sha1-Aj1vObsVyXwHHp5g0A0TbqxfoLQ= + +react-immutable-pure-component@^1.1.1: + version "1.2.3" + resolved "https://registry.npmjs.org/react-immutable-pure-component/-/react-immutable-pure-component-1.2.3.tgz#fa33638df68cfe9f73ccbee1d5861c17f3053f86" + integrity sha512-kNy2A/fDrSuR8TKwB+4ynmItmp1vgF87tWxxfmadwDYo2J3ANipHqTjDIBvJvJ7libvuh76jIbvmK0krjtKH1g== + optionalDependencies: + "@types/react" "16.4.6" + +react-inspector@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/react-inspector/-/react-inspector-2.3.1.tgz#f0eb7f520669b545b441af9d38ec6d706e5f649c" + integrity sha512-tUUK7t3KWgZEIUktOYko5Ic/oYwvjEvQUFAGC1UeMeDaQ5za2yZFtItJa2RTwBJB//NxPr000WQK6sEbqC6y0Q== + dependencies: + babel-runtime "^6.26.0" + is-dom "^1.0.9" + prop-types "^15.6.1" + react-inspector@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8" @@ -17048,7 +18585,7 @@ react-inspector@^4.0.0: is-dom "^1.0.9" prop-types "^15.6.1" -react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -17068,7 +18605,7 @@ react-lazylog@^4.5.2: text-encoding-utf-8 "^1.0.1" whatwg-fetch "^2.0.4" -react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== @@ -17087,6 +18624,15 @@ react-markdown@^4.3.1: unist-util-visit "^1.3.0" xtend "^4.0.1" +react-motion@^0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" + integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== + dependencies: + performance-now "^0.2.0" + prop-types "^15.5.8" + raf "^3.1.0" + react-popper-tooltip@^2.8.3: version "2.10.1" resolved "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-2.10.1.tgz#e10875f31916297c694d64a677d6f8fa0a48b4d1" @@ -17108,6 +18654,19 @@ react-popper@^1.3.6: typed-styles "^0.0.7" warning "^4.0.2" +react-redux@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" + integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== + dependencies: + "@babel/runtime" "^7.1.2" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.1.0" + prop-types "^15.6.1" + react-is "^16.6.0" + react-lifecycles-compat "^3.0.0" + react-redux@^7.1.1: version "7.2.0" resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" @@ -17163,6 +18722,17 @@ react-string-replace@^0.4.1: dependencies: lodash "^4.17.4" +react-syntax-highlighter@=12.2.1: + version "12.2.1" + resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz#14d78352da1c1c3f93c6698b70ec7c706b83493e" + integrity sha512-CTsp0ZWijwKRYFg9xhkWD4DSpQqE4vb2NKVMdPAkomnILSmsNBHE0n5GuI5zB+PU3ySVvXvdt9jo+ViD9XibCA== + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "~9.15.1" + lowlight "1.12.1" + prismjs "^1.8.4" + refractor "^2.4.1" + react-syntax-highlighter@^11.0.2: version "11.0.2" resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#4e3f376e752b20d2f54e4c55652fd663149e4029" @@ -17174,16 +18744,16 @@ react-syntax-highlighter@^11.0.2: prismjs "^1.8.4" refractor "^2.4.1" -react-syntax-highlighter@^13.2.1: - version "13.2.1" - resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.2.1.tgz#3d5a3b655cd85ce06b9508e0365d01ef9b8560bb" - integrity sha512-O/AF/ll4I3Dp+2WuKbnF5yKG4b1BUncqcpTW6MIBDJPr2eGKqlNGS3Nv+lyCFtAIHx8N+/ktti8qH14Q5VjjcA== +react-syntax-highlighter@^13.5.1: + version "13.5.1" + resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.1.tgz#f21737cf6d582474a0f18b06b52613f4349c0e64" + integrity sha512-VVYTnFXF55WMRGdr3QNEzAzcypFZqH45kS7rqh90+AFeNGtui8/gV5AIOIJjwTsuP2UxcO9qvEq94Jq9BYFUhw== dependencies: "@babel/runtime" "^7.3.1" highlight.js "^10.1.1" lowlight "^1.14.0" - prismjs "^1.20.0" - refractor "^3.0.0" + prismjs "^1.21.0" + refractor "^3.1.0" react-test-renderer@^16.13.1: version "16.13.1" @@ -17218,6 +18788,23 @@ react-universal-interface@^0.6.2: resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== +react-use@^12.2.0: + version "12.13.0" + resolved "https://registry.npmjs.org/react-use/-/react-use-12.13.0.tgz#dfefd8145552841f1c2213c2e79966b505a264ba" + integrity sha512-Kh0m9ezIn9xfRycx4jdAgvsYGstGfjTBO2ecIM3+G0RqrpMxTIL5jjYraHYfUzjGBHf7dUhNYBzm5vw5LItVZA== + dependencies: + "@types/react-wait" "^0.3.0" + copy-to-clipboard "^3.1.0" + nano-css "^5.1.0" + react-fast-compare "^2.0.4" + react-wait "^0.3.0" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^2.0.1" + ts-easing "^0.2.0" + tslib "^1.10.0" + react-use@^15.3.3: version "15.3.3" resolved "https://registry.npmjs.org/react-use/-/react-use-15.3.3.tgz#f16de7a16286c446388e8bd99680952fc3dc9a95" @@ -17250,7 +18837,12 @@ react-virtualized@^9.21.0: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" -react@^16.0.0, react@^16.12.0, react@^16.13.1, react@^16.8.3: +react-wait@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/react-wait/-/react-wait-0.3.0.tgz#0cdd4d919012451a5bc3ab0a16d00c6fd9a8c10b" + integrity sha512-kB5x/kMKWcn0uVr9gBdNz21/oGbQwEQnF3P9p6E9yLfJ9DRcKS0fagbgYMFI0YFOoyKDj+2q6Rwax0kTYJF37g== + +react@^16.0.0, react@^16.12.0, react@^16.13.1, react@^16.8.3, react@^16.8.4: version "16.13.1" resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== @@ -17469,7 +19061,14 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux@^4.0.4: +redux-immutable@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/redux-immutable/-/redux-immutable-3.1.0.tgz#cafbd686e0711261119b9c28960935dc47a49d0a" + integrity sha1-yvvWhuBxEmERm5wolgk13EeknQo= + dependencies: + immutable "^3.8.1" + +redux@^4.0.4, redux@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== @@ -17486,14 +19085,14 @@ refractor@^2.4.1: parse-entities "^1.1.2" prismjs "~1.17.0" -refractor@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/refractor/-/refractor-3.0.0.tgz#7c8072eaf49dbc1b333e7acc64fb52a1c9b17c75" - integrity sha512-eCGK/oP4VuyW/ERqjMZRZHxl2QsztbkedkYy/SxqE/+Gh1gLaAF17tWIOcVJDiyGhar1NZy/0B9dFef7J0+FDw== +refractor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/refractor/-/refractor-3.1.0.tgz#b05a43c8a1b4fccb30001ffcbd5cd781f7f06f78" + integrity sha512-bN8GvY6hpeXfC4SzWmYNQGLLF2ZakRDNBkgCL0vvl5hnpMrnyURk8Mv61v6pzn4/RBHzSWLp44SzMmVHqMGNww== dependencies: hastscript "^5.0.0" parse-entities "^2.0.0" - prismjs "~1.20.0" + prismjs "~1.21.0" regenerate-unicode-properties@^8.2.0: version "8.2.0" @@ -17610,6 +19209,22 @@ remark-parse@^5.0.0: vfile-location "^2.0.0" xtend "^4.0.1" +remarkable@^1.7.1: + version "1.7.4" + resolved "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" + integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg== + dependencies: + argparse "^1.0.10" + autolinker "~0.28.0" + +remarkable@^2.0.0, remarkable@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz#280ae6627384dfb13d98ee3995627ca550a12f31" + integrity sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA== + dependencies: + argparse "^1.0.10" + autolinker "^3.11.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -17631,7 +19246,7 @@ repeat-element@^1.1.2: resolved "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.5.4, repeat-string@^1.6.1: +repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -17648,6 +19263,11 @@ replace-ext@1.0.0: resolved "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + replace-in-file@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.1.0.tgz#9f9ddd7bb70d6ad231d2ad692e1b646e73d06647" @@ -17657,7 +19277,7 @@ replace-in-file@^6.0.0: glob "^7.1.6" yargs "^15.3.1" -request-progress@3.0.0: +request-progress@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= @@ -17680,7 +19300,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.55.0, request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.53.0, request@^2.55.0, request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -17726,11 +19346,21 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== +resolve-alpn@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c" + integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -17787,13 +19417,20 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12. dependencies: path-parse "^1.0.6" -responselike@^1.0.2: +responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -17889,10 +19526,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-dts@^1.4.6: - version "1.4.10" - resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.10.tgz#0373c4284a2ba4d2d72df69c289271a816bc2736" - integrity sha512-bL6MBXc8lK7D5b/tYbHaglxs4ZxMQTQilGA6Xm9KQBEj4h9ZwIDlAsvDooGjJ/cOw23r3POTRtSCEyTHxtzHJg== +rollup-plugin-dts@1.4.11: + version "1.4.11" + resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.11.tgz#aedf0b7bb91d51e20b755e2c18e840edfc7af7a1" + integrity sha512-yiScAMKgwH77b44a/IFGgjLsmwSlNfQhEM+eCb2uMrupQMPE1n/12wrnT431+v1u6wYMF1XuHqldh+v/7mTvYA== optionalDependencies: "@babel/code-frame" "^7.10.4" @@ -17912,14 +19549,14 @@ rollup-plugin-image-files@^1.4.2: rollup-pluginutils "2.4.1" rollup-plugin-peer-deps-external@^2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.2.tgz#506cef67fb68f41cca3ec08ca6ff7936b190f568" - integrity sha512-XcHH4UW9exRTAf73d8rk2dw2UgF//cWbilhRI4Ni/n+t0zA1eBtohKyJROn0fxa4/+WZ5R3onAyIDiwRQL+59A== + version "2.2.3" + resolved "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.3.tgz#059a8aec1eefb48a475e9fcedc3b9e3deb521213" + integrity sha512-W6IePXTExGXVDAlfZbNUUrx3GxUOZP248u5n4a4ID1XZMrbQ+uGeNiEfapvdzwx0qZi5DNH/hDLiPUP+pzFIxg== rollup-plugin-postcss@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-3.1.1.tgz#eb895bd919285aaf6200324071103b4d3ea68607" - integrity sha512-4/FO5/2O5kv2uWRd7PPTN4mBCWoHwwFTnpEGokfPKfj6kygvTORqkBWNgVPXi7bBefNKtMA3FqQ10se6/J8kKw== + version "3.1.8" + resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-3.1.8.tgz#d1bcaf8eb0fcb0936e3684c22dd8628d13a82fd1" + integrity sha512-JHnGfW8quNc6ePxEkZ05HEZ1YiRxDgY9RKEetMfsrwxR2kh/d90OVScTc6b1c2Q17Cs/5TRYL+1uddG21lQe3w== dependencies: chalk "^4.0.0" concat-with-sourcemaps "^1.1.0" @@ -18016,10 +19653,10 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3, rxjs@^6.5.5: - version "6.6.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84" - integrity sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg== +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3, rxjs@^6.5.5, rxjs@^6.6.0: + version "6.6.2" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== dependencies: tslib "^1.9.0" @@ -18043,6 +19680,11 @@ safe-identifier@^0.4.1: resolved "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.1.tgz#b6516bf72594f03142b5f914f4c01842ccb1b678" integrity sha512-73tOz5TXsq3apuCc3vC8c9QRhhdNZGiBhHmPPjqpH4TO5oCDqk8UIsDcSs/RG6dYcFAkOOva0pqHS3u7hh7XXA== +safe-json-parse@~1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" + integrity sha1-PnZyPjjf3aE8mx0poeB//uSzC1c= + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -18125,6 +19767,13 @@ screenfull@^5.0.0: resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7" integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ== +seek-bzip@^1.0.5: + version "1.0.6" + resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== + dependencies: + commander "^2.8.1" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -18159,6 +19808,13 @@ semver-regex@^2.0.0: resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== +semver-truncate@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz#57f41de69707a62709a7e0104ba2117109ea47e8" + integrity sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g= + dependencies: + semver "^5.3.0" + "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -18208,6 +19864,11 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" @@ -18266,6 +19927,13 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-getter@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= + dependencies: + to-object-path "^0.3.0" + set-harmonic-interval@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249" @@ -18365,10 +20033,10 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@^0.8.3: - version "0.8.3" - resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" - integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== +shelljs@^0.8.3, shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -18399,6 +20067,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -18431,6 +20113,16 @@ sisteransi@^1.0.4: resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +sitemap@^3.2.2: + version "3.2.2" + resolved "https://registry.npmjs.org/sitemap/-/sitemap-3.2.2.tgz#3f77c358fa97b555c879e457098e39910095c62b" + integrity sha512-TModL/WU4m2q/mQcrDgNANn0P4LwprM9MMvG4hu5zP4c6IIKs2YLTu6nXXnNr8ODW/WFtxKggiJ1EGn2W0GNmg== + dependencies: + lodash.chunk "^4.2.0" + lodash.padstart "^4.6.1" + whatwg-url "^7.0.0" + xmlbuilder "^13.0.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -18555,6 +20247,13 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" +sort-keys-length@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" + integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg= + dependencies: + sort-keys "^1.0.0" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -18585,7 +20284,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12: +source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12: version "0.5.19" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -18728,6 +20427,15 @@ sqlite3@^4.2.0: nan "^2.12.1" node-pre-gyp "^0.11.0" +squeak@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz#33045037b64388b567674b84322a6521073916c3" + integrity sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM= + dependencies: + chalk "^1.0.0" + console-stream "^0.1.1" + lpad-align "^1.0.1" + srcset@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac" @@ -18938,6 +20646,13 @@ stream-shift@^1.0.0: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +stream@^0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef" + integrity sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8= + dependencies: + emitter-component "^1.1.1" + streamsearch@0.1.2, streamsearch@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -18971,6 +20686,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -19050,6 +20770,11 @@ string.prototype.trimright@^2.1.1: define-properties "^1.1.3" function-bind "^1.1.1" +string_decoder@0.10: + version "0.10.31" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -19118,6 +20843,18 @@ strip-bom@^4.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== +strip-color@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz#106f65d3d3e6a2d9401cac0eb0ce8b8a702b4f7b" + integrity sha1-EG9l09PmotlAHKwOsM6LinArT3s= + +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -19157,6 +20894,13 @@ strip-json-comments@~2.0.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-outer@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + strong-log-transformer@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" @@ -19171,15 +20915,7 @@ style-inject@^0.3.0: resolved "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" integrity sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw== -style-loader@^1.0.0: - version "1.1.3" - resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz#9e826e69c683c4d9bf9db924f85e9abb30d5e200" - integrity sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw== - dependencies: - loader-utils "^1.2.3" - schema-utils "^2.6.4" - -style-loader@^1.2.1: +style-loader@^1.0.0, style-loader@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== @@ -19267,13 +21003,6 @@ supertest@^4.0.2: methods "^1.1.2" superagent "^3.8.3" -supports-color@7.1.0, supports-color@^7.0.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -19300,6 +21029,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" @@ -19313,7 +21049,7 @@ svg-parser@^2.0.0, svg-parser@^2.0.2: resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svgo@^1.0.0, svgo@^1.2.2: +svgo@^1.0.0, svgo@^1.2.2, svgo@^1.3.2: version "1.3.2" resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== @@ -19332,10 +21068,72 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -swr@^0.2.2: - version "0.2.3" - resolved "https://registry.npmjs.org/swr/-/swr-0.2.3.tgz#e0fb260d27f12fafa2388312083368f45127480d" - integrity sha512-JhuuD5ojqgjAQpZAhoPBd8Di0Mr1+ykByVKuRJdtKaxkUX/y8kMACWKkLgLQc8pcDOKEAnbIreNjU7HfqI9nHQ== +swagger-client@=3.10.12: + version "3.10.12" + resolved "https://registry.npmjs.org/swagger-client/-/swagger-client-3.10.12.tgz#f49beb7a6f91b6e5778cfbd3fd4064413be6e791" + integrity sha512-h2o7axvFViMc5sxwTjjza84Rhfz+C52wgMKPOT0P05jODjZhldBK7y9EvGt4zvqgzBJHS+FDQBmOT/dGf9SWdw== + dependencies: + "@babel/runtime-corejs2" "^7.10.4" + btoa "^1.2.1" + buffer "^5.6.0" + cookie "~0.4.1" + cross-fetch "^3.0.5" + deep-extend "~0.6.0" + fast-json-patch "^2.2.1" + isomorphic-form-data "~2.0.0" + js-yaml "^3.14.0" + lodash "^4.17.19" + qs "^6.9.4" + querystring-browser "^1.0.4" + traverse "~0.6.6" + url "~0.11.0" + +swagger-ui-react@^3.31.1: + version "3.31.1" + resolved "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-3.31.1.tgz#1770cfa1dc390f79887d641ea35ee654f4bf7d80" + integrity sha512-bGpNzizMT/7N4LTdw6yx+t3rpsEoalrn0VIfhUzv9zXVrUTl+3AKPLHpLBr0r4NvzvaZLrYuSjkatnLfngJ4vg== + dependencies: + "@babel/runtime-corejs2" "^7.10.4" + "@braintree/sanitize-url" "^4.0.0" + "@kyleshockey/object-assign-deep" "^0.4.2" + "@kyleshockey/xml" "^1.0.2" + base64-js "^1.2.0" + classnames "^2.2.6" + core-js "^2.6.11" + css.escape "1.5.1" + deep-extend "0.6.0" + dompurify "^2.0.7" + ieee754 "^1.1.13" + immutable "^3.x.x" + js-file-download "^0.4.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + memoizee "^0.4.12" + prop-types "^15.7.2" + randombytes "^2.1.0" + react-copy-to-clipboard "5.0.1" + react-debounce-input "^3.2.0" + react-immutable-proptypes "2.1.0" + react-immutable-pure-component "^1.1.1" + react-inspector "^2.3.0" + react-motion "^0.5.2" + react-redux "^5.1.2" + react-syntax-highlighter "=12.2.1" + redux "^4.0.5" + redux-immutable "3.1.0" + remarkable "^2.0.1" + reselect "^4.0.0" + serialize-error "^2.1.0" + sha.js "^2.4.11" + swagger-client "=3.10.12" + url-parse "^1.4.7" + xml-but-prettier "^1.0.1" + zenscroll "^4.0.2" + +swr@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/swr/-/swr-0.3.0.tgz#69602953dd50ab07fe60c920fd63199256645675" + integrity sha512-3p0p5TWH0qiaKAph5wBkMwqe2WjNseITfjmdVoNzjqRZGn/gnpRi6whMDjhMVb/vp/yyDtKWPlyjid8QZH+UhA== dependencies: fast-deep-equal "2.0.1" @@ -19357,10 +21155,10 @@ symbol.prototype.description@^1.0.0: es-abstract "^1.17.0-next.1" has-symbols "^1.0.1" -synchronous-promise@^2.0.10: - version "2.0.11" - resolved "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.11.tgz#e92022b0754e916f556d3ace1626d24a24214b7e" - integrity sha512-8/L5FOCjnlK0FoAfj+NqdCaImMKvEyOEzGmdfcezKp5K9HIukm4akx72endvM87eS/gU8kOxiMQflpC/CdkQAg== +synchronous-promise@^2.0.13: + version "2.0.13" + resolved "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702" + integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA== table@^5.2.3: version "5.4.6" @@ -19397,7 +21195,7 @@ tar-fs@~2.0.1: pump "^3.0.0" tar-stream "^2.0.0" -tar-stream@^1.1.2: +tar-stream@^1.1.2, tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== @@ -19451,6 +21249,21 @@ tarn@^3.0.0: resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.0.tgz#a4082405216c0cce182b8b4cb2639c52c1e870d4" integrity sha512-PKUnlDFODZueoA8owLehl8vLcgtA8u4dRuVbZc92tspDYZixjJL6TqYOmryf/PfP/EBX+2rgNcrj96NO+RPkdQ== +tcp-port-used@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70" + integrity sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q== + dependencies: + debug "4.1.0" + is2 "2.0.1" + +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + telejson@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/telejson/-/telejson-3.3.0.tgz#6d814f3c0d254d5c4770085aad063e266b56ad03" @@ -19465,6 +21278,20 @@ telejson@^3.2.0: lodash "^4.17.15" memoizerific "^1.11.3" +telejson@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/telejson/-/telejson-5.0.2.tgz#ed1e64be250cc1c757a53c19e1740b49832b3d51" + integrity sha512-XCrDHGbinczsscs8LXFr9jDhvy37yBk9piB7FJrCfxE8oP66WDkolNMpaBkWYgQqB9dQGBGtTDzGQPedc9KJmw== + dependencies: + "@types/is-function" "^1.0.0" + global "^4.4.0" + is-function "^1.0.2" + is-regex "^1.1.1" + is-symbol "^1.0.3" + isobject "^4.0.0" + lodash "^4.17.19" + memoizerific "^1.11.3" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" @@ -19482,6 +21309,14 @@ temp-write@^3.4.0: temp-dir "^1.0.0" uuid "^3.0.1" +tempfile@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz#6b0446856a9b1114d1856ffcbe509cccb0977265" + integrity sha1-awRGhWqbERTRhW/8vlCczLCXcmU= + dependencies: + temp-dir "^1.0.0" + uuid "^3.0.1" + term-size@^2.1.0: version "2.2.0" resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" @@ -19594,10 +21429,10 @@ throat@^5.0.0: resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== -throttle-debounce@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5" - integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg== +throttle-debounce@^2.0.1, throttle-debounce@^2.1.0: + version "2.2.1" + resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137" + integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA== throttleit@^1.0.0: version "1.0.0" @@ -19639,6 +21474,11 @@ timeago.js@^4.0.2: resolved "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz#724e8c8833e3490676c7bb0a75f5daf20e558028" integrity sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w== +timed-out@^4.0.0, timed-out@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + timers-browserify@^2.0.4: version "2.0.11" resolved "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" @@ -19646,6 +21486,14 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +timers-ext@^0.1.5: + version "0.1.7" + resolved "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" + integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + dependencies: + es5-ext "~0.10.46" + next-tick "1" + timsort@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" @@ -19661,18 +21509,28 @@ tiny-invariant@^1.0.6: resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== +tiny-lr@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" + integrity sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA== + dependencies: + body "^5.1.0" + debug "^3.1.0" + faye-websocket "~0.10.0" + livereload-js "^2.3.0" + object-assign "^4.1.0" + qs "^6.4.0" + +tiny-merge-patch@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/tiny-merge-patch/-/tiny-merge-patch-0.1.2.tgz#2e8ded19c56ea15dbd3ad4ed5db1c8e5ad544c3c" + integrity sha1-Lo3tGcVuoV29OtTtXbHI5a1UTDw= + tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tmp@0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== - dependencies: - rimraf "^2.6.3" - tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -19680,6 +21538,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@~0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" + integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== + dependencies: + rimraf "^2.6.3" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -19712,6 +21577,11 @@ to-readable-stream@^1.0.0: resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== +to-readable-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" + integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -19747,6 +21617,11 @@ toidentifier@1.0.0: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toml@^2.3.2: + version "2.3.6" + resolved "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b" + integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ== + toposort@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -19800,11 +21675,23 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +traverse@~0.6.6: + version "0.6.6" + resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= + tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +tree-node-cli@^1.2.5: + version "1.4.0" + resolved "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.4.0.tgz#8f4028554610d6ee1cdeb98554a60841a3cfa3ac" + integrity sha512-hBc/cp7rTSHFSFvaTzmHNYyJv87UJBsxsfCoq2DtDQuMES4vhnLuvXZit/asGtZG8edWTCydWeFWoBz9LYkJdQ== + dependencies: + commander "^5.0.0" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -19820,6 +21707,13 @@ trim-off-newlines@^1.0.0: resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= + dependencies: + escape-string-regexp "^1.0.2" + trim-trailing-lines@^1.0.0: version "1.1.3" resolved "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94" @@ -19840,12 +21734,20 @@ trough@^1.0.0: resolved "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +truncate-html@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/truncate-html/-/truncate-html-1.0.3.tgz#0166dfc7890626130c2e4174c6b73d4d63993e5f" + integrity sha512-1o1prdRv+iehXcGwn29YgXU17DotHkr+OK3ijVEG7FGMwHNG9RyobXwimw6djDvbIc24rhmz3tjNNvNESjkNkQ== + dependencies: + "@types/cheerio" "^0.22.8" + cheerio "0.22.0" + tryer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== -ts-dedent@^1.1.0: +ts-dedent@^1.1.0, ts-dedent@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-1.1.1.tgz#68fad040d7dbd53a90f545b450702340e17d18f3" integrity sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg== @@ -19978,6 +21880,11 @@ type-detect@4.0.8: resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.10.0: + version "0.10.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" + integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== + type-fest@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" @@ -20034,9 +21941,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typescript@^3.9.3: - version "3.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + version "3.9.7" + resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -20066,6 +21973,14 @@ umask@^1.1.0: resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= +unbzip2-stream@^1.0.9: + version "1.4.3" + resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -20252,7 +22167,7 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -untildify@4.0.0: +untildify@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== @@ -20281,7 +22196,7 @@ update-notifier@^4.0.0: semver-diff "^3.1.1" xdg-basedir "^4.0.0" -uri-js@^4.2.2: +uri-js@^4.2.1, uri-js@^4.2.2: version "4.2.2" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== @@ -20311,6 +22226,13 @@ url-loader@^4.1.0: mime-types "^2.1.26" schema-utils "^2.6.5" +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= + dependencies: + prepend-http "^1.0.1" + url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" @@ -20318,7 +22240,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3: +url-parse@^1.4.3, url-parse@^1.4.7: version "1.4.7" resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== @@ -20326,7 +22248,17 @@ url-parse@^1.4.3: querystringify "^2.1.1" requires-port "^1.0.0" -url@0.11.0, url@^0.11.0: +url-to-options@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" + integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= + +url-value-parser@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.0.1.tgz#c8179a095ab9ec1f5aa17ca36af5af396b4e95ed" + integrity sha512-bexECeREBIueboLGM3Y1WaAzQkIn+Tca/Xjmjmfd0S/hFHSCEoFkNh0/D0l9G4K74MkEP/lLFRlYnxX3d68Qgw== + +url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= @@ -20421,34 +22353,29 @@ uuid@^7.0.3: resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== -uuid@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" - integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== - -uuid@^8.2.0: - version "8.2.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e" - integrity sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q== +uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== -v8-to-istanbul@^4.1.3: - version "4.1.4" - resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz#b97936f21c0e2d9996d4985e5c5156e9d4e49cd6" - integrity sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ== +v8-to-istanbul@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz#0608f5b49a481458625edb058488607f25498ba5" + integrity sha512-mbDNjuDajqYe3TXFk5qxcQy8L1msXNE37WTlLoqqpBfRsimbNcrlhQlDPntmECEcUvdC+AQ8CyMMf6EUx1r74Q== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" source-map "^0.7.3" -v8flags@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz#fc9dc23521ca20c5433f81cc4eb9b3033bb105d8" - integrity sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w== +v8flags@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" + integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== dependencies: homedir-polyfill "^1.0.1" @@ -20615,6 +22542,13 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webapi-parser@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/webapi-parser/-/webapi-parser-0.5.0.tgz#2632185c5d8f3e6addb2520857af89ea9844dc14" + integrity sha512-fPt6XuMqLSvBz8exwX4QE1UT+pROLHa00EMDCdO0ybICduwQ1V4f7AWX4pNOpCp+x+0FjczEsOxtQU0d8L3QKw== + dependencies: + ajv "6.5.2" + webidl-conversions@^3.0.0, webidl-conversions@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -20703,10 +22637,10 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-node-externals@^1.7.2: - version "1.7.2" - resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-1.7.2.tgz#6e1ee79ac67c070402ba700ef033a9b8d52ac4e3" - integrity sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg== +webpack-node-externals@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.0.tgz#8d50f3289c71bc2b921a8da228e0b652acc503f1" + integrity sha512-g7/Z7Q/gsP8GkJkKZuJggn6RSb5PvxW1YD5vvmRZIxaSxAzkqjfL5n9CslVmNYlSqBVCyiqFgOqVS2IOObCSRg== webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" @@ -20791,16 +22725,16 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@3.0.0, whatwg-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" - integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== - -whatwg-fetch@^2.0.0, whatwg-fetch@^2.0.4: +whatwg-fetch@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== +whatwg-fetch@^3.0.0, whatwg-fetch@^3.4.0: + version "3.4.0" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz#e11de14f4878f773fbebcde8871b2c0699af8b30" + integrity sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ== + whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" @@ -20924,6 +22858,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -21069,6 +23008,13 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xml-but-prettier@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/xml-but-prettier/-/xml-but-prettier-1.0.1.tgz#f5a33267ed42ccd4e355c62557a5e39b01fb40f3" + integrity sha1-9aMyZ+1CzNTjVcYlV6XjmwH7QPM= + dependencies: + repeat-string "^1.5.2" + xml-crypto@^1.4.0: version "1.5.3" resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34" @@ -21087,6 +23033,13 @@ xml-encryption@^1.0.0: xmldom "~0.1.15" xpath "0.0.27" +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + "xml-name-validator@>= 2.0.1 < 3.0.0": version "2.0.1" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" @@ -21110,6 +23063,11 @@ xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlbuilder@^13.0.0: + version "13.0.2" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" + integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -21165,6 +23123,11 @@ yaeti@^0.0.6: resolved "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc= +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.1.1" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -21175,12 +23138,25 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml-ast-parser@0.0.43: + version "0.0.43" + resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" + integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== + yaml@*, yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: version "1.10.0" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@18.x, yargs-parser@^18.1.1: +yamljs@^0.2.1: + version "0.2.10" + resolved "https://registry.npmjs.org/yamljs/-/yamljs-0.2.10.tgz#481cc7c25ca73af59f591f0c96e3ce56c757a40f" + integrity sha1-SBzHwlynOvWfWR8MluPOVsdXpA8= + dependencies: + argparse "^1.0.7" + glob "^7.0.5" + +yargs-parser@18.x, yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -21252,10 +23228,10 @@ yargs@^14.2.2: y18n "^4.0.0" yargs-parser "^15.0.1" -yargs@^15.3.1: - version "15.3.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" - integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== +yargs@^15.3.1, yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" decamelize "^1.2.0" @@ -21267,7 +23243,14 @@ yargs@^15.3.1: string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^18.1.1" + yargs-parser "^18.1.2" + +yargs@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-2.3.0.tgz#e900c87250ec5cd080db6009fe3dd63156f1d7fb" + integrity sha1-6QDIclDsXNCA22AJ/j3WMVbx1/s= + dependencies: + wordwrap "0.0.2" yargs@^5.0.0: version "5.0.0" @@ -21289,7 +23272,7 @@ yargs@^5.0.0: y18n "^3.2.1" yargs-parser "^3.2.0" -yauzl@2.10.0, yauzl@^2.10.0: +yauzl@^2.10.0, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= @@ -21316,16 +23299,16 @@ yn@^4.0.0: integrity sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg== yup@^0.29.1: - version "0.29.1" - resolved "https://registry.npmjs.org/yup/-/yup-0.29.1.tgz#35d25aab470a0c3950f66040ba0ff4b1b6efe0d9" - integrity sha512-U7mPIbgfQWI6M3hZCJdGFrr+U0laG28FxMAKIgNvgl7OtyYuUoc4uy9qCWYHZjh49b8T7Ug8NNDdiMIEytcXrQ== + version "0.29.3" + resolved "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea" + integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ== dependencies: - "@babel/runtime" "^7.9.6" + "@babel/runtime" "^7.10.5" fn-name "~3.0.0" lodash "^4.17.15" lodash-es "^4.17.11" property-expr "^2.0.2" - synchronous-promise "^2.0.10" + synchronous-promise "^2.0.13" toposort "^2.0.2" zen-observable-ts@^0.8.21: @@ -21341,6 +23324,11 @@ zen-observable@^0.8.0, zen-observable@^0.8.15: resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== +zenscroll@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz#e8d5774d1c0738a47bcfa8729f3712e2deddeb25" + integrity sha1-6NV3TRwHOKR7z6hynzcS4t7d6yU= + zombie@^6.1.4: version "6.1.4" resolved "https://registry.npmjs.org/zombie/-/zombie-6.1.4.tgz#9f0f53f3d9a032beb7f3fe5b382146a3475a4d47"