diff --git a/.changeset/afraid-dingos-own.md b/.changeset/afraid-dingos-own.md new file mode 100644 index 0000000000..9f7010d330 --- /dev/null +++ b/.changeset/afraid-dingos-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +pass registered logger to requestLoggingHandler diff --git a/.changeset/afraid-teachers-cross.md b/.changeset/afraid-teachers-cross.md deleted file mode 100644 index 8fc376441f..0000000000 --- a/.changeset/afraid-teachers-cross.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Pass on plugin database management instance that is now required by the scaffolder plugin. - -To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`: - -```diff -export default async function createPlugin({ - logger, - config, -+ database, -}: PluginEnvironment) { - -// ...omitted... - - return await createRouter({ - preparers, - templaters, - publishers, - logger, - config, - dockerClient, - entityClient, -+ database, - }); -} -``` diff --git a/.changeset/cost-insights-tidy-geese-play.md b/.changeset/cost-insights-tidy-geese-play.md deleted file mode 100644 index f8aa503ca6..0000000000 --- a/.changeset/cost-insights-tidy-geese-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -Fix snooze quarter option diff --git a/.changeset/curly-crabs-compete.md b/.changeset/curly-crabs-compete.md deleted file mode 100644 index 552a41a9f1..0000000000 --- a/.changeset/curly-crabs-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`. diff --git a/.changeset/cyan-lies-flow.md b/.changeset/cyan-lies-flow.md deleted file mode 100644 index e7da9fab0a..0000000000 --- a/.changeset/cyan-lies-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': minor ---- - -Migrated to new composability API, exporting the plugin instance as `searchPlugin`, and page as `SearchPage`. Due to the old router component also being called `SearchPage`, this is a breaking change. The old page component is now exported as `Router`, which can be used to maintain the old behavior. diff --git a/.changeset/early-hotels-mate.md b/.changeset/early-hotels-mate.md deleted file mode 100644 index 2cda0344bf..0000000000 --- a/.changeset/early-hotels-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -use child logger, if provided, to log single location refresh diff --git a/.changeset/empty-cooks-laugh.md b/.changeset/empty-cooks-laugh.md deleted file mode 100644 index 8f509f08cd..0000000000 --- a/.changeset/empty-cooks-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-backend': patch ---- - -Added a new `disableConfigInjection` option, which can be used to disable the configuration injection in environments where it can't be used. diff --git a/.changeset/fluffy-hats-hug.md b/.changeset/fluffy-hats-hug.md deleted file mode 100644 index b3d06337fd..0000000000 --- a/.changeset/fluffy-hats-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Don't respond to a request twice if an entity has not been found. diff --git a/.changeset/four-rings-push.md b/.changeset/four-rings-push.md deleted file mode 100644 index 500dd0e812..0000000000 --- a/.changeset/four-rings-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cloudbuild': patch ---- - -Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`. diff --git a/.changeset/funny-students-shout.md b/.changeset/funny-students-shout.md deleted file mode 100644 index 97b273c687..0000000000 --- a/.changeset/funny-students-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-register-component': patch ---- - -Migrated to new composability API, exporting the plugin instance as `registerComponentPlugin`, and page as `RegisterComponentPage`. diff --git a/.changeset/grumpy-cups-hope.md b/.changeset/grumpy-cups-hope.md new file mode 100644 index 0000000000..6445c07efa --- /dev/null +++ b/.changeset/grumpy-cups-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Use routed tabs to link to every settings page. diff --git a/.changeset/hot-pants-taste.md b/.changeset/hot-pants-taste.md deleted file mode 100644 index 5cce26c5bf..0000000000 --- a/.changeset/hot-pants-taste.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-techdocs': patch ---- - -Improve display of error messages diff --git a/.changeset/large-terms-invite.md b/.changeset/large-terms-invite.md deleted file mode 100644 index 6653ac8e1e..0000000000 --- a/.changeset/large-terms-invite.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend. - -This API should be considered unstable until used by the scaffolder frontend. diff --git a/.changeset/ninety-houses-shout.md b/.changeset/ninety-houses-shout.md deleted file mode 100644 index 845decc74c..0000000000 --- a/.changeset/ninety-houses-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`. diff --git a/.changeset/ninety-lemons-shake.md b/.changeset/ninety-lemons-shake.md new file mode 100644 index 0000000000..960a4eaffe --- /dev/null +++ b/.changeset/ninety-lemons-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Limit the props that are forwarded to the `Link` component in the `EntityRefLink`. diff --git a/.changeset/odd-buckets-compare.md b/.changeset/odd-buckets-compare.md deleted file mode 100644 index faf68fb4a0..0000000000 --- a/.changeset/odd-buckets-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Set explicit content-type in error handler responses. diff --git a/.changeset/old-tools-exist.md b/.changeset/old-tools-exist.md deleted file mode 100644 index a9365cb784..0000000000 --- a/.changeset/old-tools-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around. diff --git a/.changeset/perfect-ladybugs-listen.md b/.changeset/perfect-ladybugs-listen.md deleted file mode 100644 index 2f9e65c1d9..0000000000 --- a/.changeset/perfect-ladybugs-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-pagerduty': patch ---- - -Migrated to new composability API, exporting the plugin instance as `pagerDutyPlugin`, entity card as `EntityPagerDutyCard`, and entity conditional as `isPagerDutyAvailable`. diff --git a/.changeset/poor-sheep-give.md b/.changeset/poor-sheep-give.md deleted file mode 100644 index 4c52594b35..0000000000 --- a/.changeset/poor-sheep-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/test-utils': patch ---- - -Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components. diff --git a/.changeset/popular-cars-eat.md b/.changeset/popular-cars-eat.md deleted file mode 100644 index 6d3bcdfab2..0000000000 --- a/.changeset/popular-cars-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins': patch ---- - -Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`. diff --git a/.changeset/selfish-years-pump.md b/.changeset/selfish-years-pump.md deleted file mode 100644 index e5cc5f3bc3..0000000000 --- a/.changeset/selfish-years-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-github-actions': patch ---- - -Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGithubActionsContent`, entity conditional as `isGithubActionsAvailable`, and entity cards as `EntityLatestGithubActionRunCard`, `EntityLatestGithubActionsForBranchCard`, and `EntityRecentGithubActionsRunsCard`. diff --git a/.changeset/shiny-falcons-marry.md b/.changeset/shiny-falcons-marry.md deleted file mode 100644 index 4dda68d693..0000000000 --- a/.changeset/shiny-falcons-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-newrelic': patch ---- - -Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`. diff --git a/.changeset/shiny-shirts-worry.md b/.changeset/shiny-shirts-worry.md deleted file mode 100644 index b4797b61b4..0000000000 --- a/.changeset/shiny-shirts-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Implement `UrlReader.search` which implements glob matching. diff --git a/.changeset/shy-maps-guess.md b/.changeset/shy-maps-guess.md deleted file mode 100644 index 2a50d3d461..0000000000 --- a/.changeset/shy-maps-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -Migrate to new composability API, exporting the plugin instance as `kubernetesPlugin` and entity content as `EntityKubernetesContent`. diff --git a/.changeset/sixty-lemons-agree.md b/.changeset/sixty-lemons-agree.md deleted file mode 100644 index 7fc438b433..0000000000 --- a/.changeset/sixty-lemons-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kafka': patch ---- - -Migrate to new composability API, exporting the plugin instance as `kafkaPlugin`, entity content as `EntityKafkaContent`, and entity conditional as `isKafkaAvailable`. diff --git a/.changeset/strange-cobras-unite.md b/.changeset/strange-cobras-unite.md deleted file mode 100644 index d53a324dab..0000000000 --- a/.changeset/strange-cobras-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Refactored route response handling to use more explicit types and throw errors. diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md new file mode 100644 index 0000000000..7490f6096a --- /dev/null +++ b/.changeset/strange-olives-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Migrated the package from using moment to Luxon. #4278 diff --git a/.changeset/stupid-maps-do.md b/.changeset/stupid-maps-do.md new file mode 100644 index 0000000000..1cbac64f68 --- /dev/null +++ b/.changeset/stupid-maps-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Export Select component diff --git a/.changeset/techdocs-green-rabbits-burn.md b/.changeset/techdocs-green-rabbits-burn.md deleted file mode 100644 index 5af68c2000..0000000000 --- a/.changeset/techdocs-green-rabbits-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Enhance API calls to support trapping 500 errors from techdocs-backend diff --git a/.changeset/thick-scissors-notice.md b/.changeset/thick-scissors-notice.md new file mode 100644 index 0000000000..ccf78f3abf --- /dev/null +++ b/.changeset/thick-scissors-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Make sure that SidebarItems are also active when on sub route. diff --git a/.changeset/tough-worms-clap.md b/.changeset/tough-worms-clap.md new file mode 100644 index 0000000000..3a18593549 --- /dev/null +++ b/.changeset/tough-worms-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing. diff --git a/.changeset/tricky-dancers-rush.md b/.changeset/tricky-dancers-rush.md deleted file mode 100644 index 301b23d98e..0000000000 --- a/.changeset/tricky-dancers-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Fixed type inference of `createRouteRef`. diff --git a/.changeset/two-dogs-search.md b/.changeset/two-dogs-search.md deleted file mode 100644 index 5f673c0cfb..0000000000 --- a/.changeset/two-dogs-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-gitops-profiles': patch ---- - -Migrated to new composability API, exporting the plugin instance as `gitopsProfilesPlugin` and pages as `GitopsProfilesClusterListPage`, `GitopsProfilesClusterPage`, and `GitopsProfilesCreatePage`. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a5ef09554..371fae6e6b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,6 +7,7 @@ * @backstage/maintainers /docs/features/techdocs @backstage/techdocs-core /docs/features/search @backstage/techdocs-core +/docs/assets/search @backstage/techdocs-core /plugins/cost-insights @backstage/silver-lining /plugins/cloudbuild @trivago/ebarrios /plugins/search @backstage/techdocs-core diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 37bfd9af92..519216ecd8 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -250,6 +250,7 @@ transpilation Tuite ui unmanaged +unregister untracked upvote url diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa2feebb3e..08ec67162b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: run: yarn prettier:check - name: validate config - run: yarn backstage-cli config:check + run: yarn backstage-cli config:check --lax - name: lint run: yarn lerna -- run lint --since origin/master diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 425e1172a4..60db9ac16b 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -65,7 +65,7 @@ jobs: COMMIT_SHA_BEFORE: '${{ github.event.before }}' - name: validate config - run: yarn backstage-cli config:check + run: yarn backstage-cli config:check --lax - name: lint run: yarn lerna -- run lint diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index fdb2e1ddc4..bb5257b7c6 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -48,11 +48,14 @@ jobs: target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: '14' + - name: yarn install + run: yarn --cwd cypress install + # This is required because the environment_url param that Tugboat uses # to tell us where the preview is located isn't supported unless you # specify the custom Accept header when getting the deployment_status, @@ -77,9 +80,25 @@ jobs: }); console.log(result); return result.data.environment_url; - - name: echo tugboat preview url - run: | - curl ${{steps.get-status-env.outputs.result}} + + - name: cypress run + uses: cypress-io/github-action@v2 + env: + CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}} + with: + config-file: ./cypress.json + working-directory: ./cypress + browser: chrome + install: false + headless: true + + - name: update artifact + if: ${{ always() }} + uses: actions/upload-artifact@v1 + with: + name: cypress-videos + path: ./cypress/cypress/videos + - name: set status if: ${{ failure() }} uses: actions/github-script@v3 @@ -94,6 +113,7 @@ jobs: context: 'Backstage Tugboat E2E Tests', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); + - name: set status if: ${{ success() }} uses: actions/github-script@v3 diff --git a/README.md b/README.md index 7c04672998..d01be3f54c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates - [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter +- [Backstage Community Sessions](https://github.com/backstage/community) - Join monthly meetup and explore Backstage community - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ ## License diff --git a/app-config.yaml b/app-config.yaml index 5240a050a8..9df6540a2c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -73,6 +73,9 @@ organization: name: My Company # Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach techdocs: builder: 'local' # Alternatives - 'external' generators: @@ -305,6 +308,10 @@ auth: $env: AUTH_OAUTH2_AUTH_URL tokenUrl: $env: AUTH_OAUTH2_TOKEN_URL + ### + # provide a list of scopes as needed for your OAuth2 Server: + # + # scope: saml-login-selector openid profile email oidc: development: metadataUrl: diff --git a/catalog-info.yaml b/catalog-info.yaml index 7e60af5755..0cab0c558a 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -4,6 +4,15 @@ metadata: name: backstage description: | Backstage is an open-source developer portal that puts the developer experience first. + links: + - title: Website + url: http://backstage.io + - title: Documentation + url: https://backstage.io/docs + - title: Storybook + url: https://backstage.io/storybook + - title: Discord Chat + url: https://discord.com/invite/EBHEGzX annotations: github.com/project-slug: backstage/backstage backstage.io/techdocs-ref: url:https://github.com/backstage/backstage diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js new file mode 100644 index 0000000000..d7498649a0 --- /dev/null +++ b/cypress/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], + rules: { + 'no-console': 0, + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + optionalDependencies: false, + peerDependencies: false, + bundledDependencies: false, + }, + ], + 'jest/expect-expect': 'off', + 'no-restricted-syntax': 'off', + }, +}; diff --git a/cypress/README.md b/cypress/README.md new file mode 100644 index 0000000000..2bb310e300 --- /dev/null +++ b/cypress/README.md @@ -0,0 +1,27 @@ +# Cypress Tests for Backstage + +Hey 👋 Welcome to the Cypress tests for Backstage. They're designed to be run against the `packages/app` folder in the main repo, and be some form of smoke tests to make sure that we don't break any core functionality. + +They run part of the PR build, and are triggered from the `.github/workflows/tugboat.yml` file. + +The main app gets built up part of a [Tugboat Build](https://tugboat.qa), which when complete, sends a `deployment event` to the PR triggering the aforementioned workflow. + +### Running Locally + +In order to make typescript happy, this `cypress` package is separate from all the Jest dependencies in the monorepo workspaces setup. + +You can run the e2e tests locally pointing at your local running version like so: + +```sh +cd cypress +yarn install +yarn cypress run +``` + +You can open up the `cypress` console by using `yarn cypress open`. + +You can also run towards any Backstage installation by using the Cypress Environment Variable overrides. + +```sh +CYPRESS_baseUrl="http://demo.backstage.io" yarn cypress run +``` diff --git a/cypress/cypress.json b/cypress/cypress.json new file mode 100644 index 0000000000..ebbbe59901 --- /dev/null +++ b/cypress/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:7000", + "integrationFolder": "./src/integration", + "supportFile": "./src/support", + "fixturesFolder": "./src/fixures", + "pluginsFile": "./src/plugins", + "defaultCommandTimeout": 10000 +} diff --git a/cypress/package.json b/cypress/package.json new file mode 100644 index 0000000000..8549386f7e --- /dev/null +++ b/cypress/package.json @@ -0,0 +1,11 @@ +{ + "name": "@backstage/cypress-tests", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "dependencies": { + "cypress": "^6.4.0", + "typescript": "^4.1.3" + } +} diff --git a/cypress/src/fixtures/.gitkeep b/cypress/src/fixtures/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cypress/src/integration/catalog.ts b/cypress/src/integration/catalog.ts new file mode 100644 index 0000000000..31d39e7efd --- /dev/null +++ b/cypress/src/integration/catalog.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 'os'; + +describe('Catalog', () => { + describe('default entities', () => { + it('displays the correct amount of entities from default config', () => { + cy.loginAsGuest(); + + cy.visit('/catalog'); + + cy.contains('Owned (7)').should('be.visible'); + }); + }); +}); diff --git a/cypress/src/plugins/index.ts b/cypress/src/plugins/index.ts new file mode 100644 index 0000000000..b90c276d71 --- /dev/null +++ b/cypress/src/plugins/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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 () => {}; diff --git a/cypress/src/support/index.ts b/cypress/src/support/index.ts new file mode 100644 index 0000000000..e17081831e --- /dev/null +++ b/cypress/src/support/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2021 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. + */ +/// + +Cypress.Commands.add('loginAsGuest', () => { + cy.visit('/', { + onLoad: (win: Window) => + win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'), + }); +}); + +export {}; diff --git a/plugins/catalog-import/src/util/urls.ts b/cypress/src/types.d.ts similarity index 62% rename from plugins/catalog-import/src/util/urls.ts rename to cypress/src/types.d.ts index c51dc0a999..361aaba9f3 100644 --- a/plugins/catalog-import/src/util/urls.ts +++ b/cypress/src/types.d.ts @@ -13,19 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import parseGitUrl from 'git-url-parse'; - -export type UrlType = 'file' | 'tree'; - -export function urlType(url: string): UrlType { - const { filepathtype, filepath } = parseGitUrl(url); - - if (filepathtype === 'tree' || filepathtype === 'file') { - return filepathtype; - } else if (filepath?.match(/\.ya?ml$/)) { - return 'file'; +declare module 'zombie'; +declare module 'pgtools'; +declare namespace Cypress { + interface Chainable { + /** + * Login as guest + * @example cy.loginAsGuests + */ + loginAsGuest(): Chainable; } - - return 'tree'; } diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000000..d9b4869ecd --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "allowJs": true, + "declaration": true, + "declarationMap": false, + "esModuleInterop": true, + "experimentalDecorators": false, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, + "incremental": true, + "isolatedModules": true, + "jsx": "react", + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"], + "module": "ESNext", + "moduleResolution": "node", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "sourceMap": false, + "skipLibCheck": false, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2019", + "types": ["node", "cypress"] + } +} diff --git a/cypress/yarn.lock b/cypress/yarn.lock new file mode 100644 index 0000000000..3c737f6ebc --- /dev/null +++ b/cypress/yarn.lock @@ -0,0 +1,1409 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@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= + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +"@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== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +"@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== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + +"@samverschueren/stream-to-observable@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" + integrity sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ== + dependencies: + any-observable "^0.3.0" + +"@types/sinonjs__fake-timers@^6.0.1": + version "6.0.2" + resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" + integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + +"@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== + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +any-observable@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" + integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== + +arch@^2.1.2: + version "2.2.0" + resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +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== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +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== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^1.0.0, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.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== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.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= + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= + dependencies: + restore-cursor "^1.0.1" + +cli-cursor@^2.0.0, cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + +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" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colors@^1.1.2: + version "1.4.0" + resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.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: + version "1.8.0" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +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== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cypress@^6.4.0: + version "6.4.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2" + integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg== + 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" + blob-util "2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.9.3" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^4.0.2" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.0.1" + 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 "^4.0.0" + minimist "^1.2.5" + moment "^2.29.1" + ospath "^1.2.2" + pretty-bytes "^5.4.1" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.2.0" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date-fns@^1.27.2: + version "1.30.1" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + +dayjs@^1.9.3: + version "1.10.4" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" + integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1: + version "4.3.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +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== + +execa@^4.0.2: + version "4.1.0" + resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.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== + dependencies: + pify "^2.2.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +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== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +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" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +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= + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +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== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.5" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz#bc18864a6c9fc7b303f2e2abdb9155ad178fbe29" + integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@1.3.7: + version "1.3.7" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + +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== + dependencies: + ci-info "^2.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "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-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== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-observable@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" + integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== + dependencies: + symbol-observable "^1.1.0" + +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + +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-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= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +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" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.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= + +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" + integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= + +listr-update-renderer@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" + integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^2.3.0" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" + integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== + dependencies: + chalk "^2.4.1" + cli-cursor "^2.1.0" + date-fns "^1.27.2" + figures "^2.0.0" + +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== + dependencies: + "@samverschueren/stream-to-observable" "^0.3.0" + is-observable "^1.1.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.5.0" + listr-verbose-renderer "^0.5.0" + p-map "^2.0.0" + rxjs "^6.3.3" + +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@^4.17.19: + 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" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= + dependencies: + chalk "^1.0.0" + +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +log-update@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" + integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= + dependencies: + ansi-escapes "^3.0.0" + cli-cursor "^2.0.0" + wrap-ansi "^3.0.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.28" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + dependencies: + mime-db "1.45.0" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +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== + +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.29.1: + version "2.29.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +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-map@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pretty-bytes@^5.4.1: + version "5.5.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +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== + +readable-stream@^2.2.2: + version "2.3.7" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +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= + dependencies: + throttleit "^1.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" + integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rxjs@^6.3.3: + version "6.6.3" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" + integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0, supports-color@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +symbol-observable@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@^4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +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== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.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= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" + integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index ff322c136f..5b5656f1b1 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -119,7 +119,7 @@ Plugins supply their APIs through the `apis` option of `createPlugin`, for example: ```ts -export const plugin = createPlugin({ +export const techdocsPlugin = createPlugin({ id: 'techdocs', apis: [ createApiFactory({ diff --git a/docs/assets/search/architecture.drawio.svg b/docs/assets/search/architecture.drawio.svg index af04ab63e8..a624a375e1 100644 --- a/docs/assets/search/architecture.drawio.svg +++ b/docs/assets/search/architecture.drawio.svg @@ -1,13 +1,13 @@ - + - + - + @@ -66,24 +66,26 @@ - + - - + + - @backstage/plugin-search-backend + @backstage/ + + plugin-search-backend - - @backstage/plugin-search-backend + + @backstage/... - + @@ -138,9 +140,9 @@ - - - + + + @@ -266,7 +268,7 @@ - + Pass Search @@ -280,7 +282,7 @@ - + Pass Search... @@ -352,8 +354,8 @@ - - Gather Documents + + Gather Documents From Plugins @@ -366,7 +368,7 @@ - + @@ -375,16 +377,14 @@ - Collate Documents - - Or Metadata + Register Document / Metadata Collation Handler(s) - Collate Docum... + Register Docu... @@ -427,11 +427,11 @@ - + - + @@ -441,7 +441,7 @@ - + Index Processi... @@ -529,10 +529,29 @@ + + + + + + + + @backstage/ + + plugin-search-indexer + + + + + + @backstage/... + + + - + Viewer does not support full SVG 1.1 diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 424869196d..a99ca119f0 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -76,14 +76,16 @@ by also providing the `cert` configuration. ### Configuration -Each authentication provider (except SAML) needs five parameters: an OAuth -client ID, a client secret, an authorization endpoint, 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. +Each authentication provider (except SAML) needs six parameters: an OAuth client +ID, a client secret, an authorization endpoint, a token endpoint, an optional +list of scopes (as a string separated by spaces) that may be required by the +OAuth2 Server to enable end-user sign-on, 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. @@ -109,6 +111,18 @@ auth: development: clientId: $env: + oauth2: + development: + clientId: + $env: AUTH_OAUTH2_CLIENT_ID + clientSecret: + $env: AUTH_OAUTH2_CLIENT_SECRET + authorizationUrl: + $env: AUTH_OAUTH2_AUTH_URL + tokenUrl: + $env: AUTH_OAUTH2_TOKEN_URL + scope: + $env: AUTH_OAUTH2_SCOPE saml: entryPoint: $env: AUTH_SAML_ENTRY_POINT diff --git a/docs/features/search/architecture.md b/docs/features/search/architecture.md index 3075719b9d..118afa6710 100644 --- a/docs/features/search/architecture.md +++ b/docs/features/search/architecture.md @@ -6,9 +6,9 @@ description: Documentation on Search Architecture # Search Architecture -> _This is a proposed architecture which has not been implemented yet. We are -> still looking for feedback to improve the architecture to fit your use-case, -> see [this open issue](https://github.com/backstage/backstage/issues/4078)._ +> _This is a proposed architecture which has not been implemented yet. Find our +> milestones to follow our progress on the +> [Search Roadmap](./README.md#project-roadmap)._ Below you can explore the Search Architecture. Our aim with this architecture is to support a wide variety of search engines, while providing a simple developer @@ -20,7 +20,10 @@ Backstage end-users. At a base-level, we want to support the following: - We aim to enable the capability to search across the entire Backstage - ecosystem by decoupling search from content management. + ecosystem including, but not limited to, entities in the software catalog. + Searchable content won't be required to relate directly to the software + catalog, but by convention, we may encourage loose relationships using + well-known field names or attributes. - We aim to enable the capability to deploy Backstage using any search engine, by providing an integration and translation layer between the core search plugin and search engine specific logic that can be extended for different @@ -29,11 +32,17 @@ At a base-level, we want to support the following: More advanced use-cases we hope to support with this architecture include: -- It should be easy for any plugin to expose new content to search. (e.g. entity - metadata, documentation from TechDocs) -- It should be easy for any plugin to append relevant metadata to existing +- It should be possible for any plugin to expose new content to search. (e.g. + entity metadata, documentation from TechDocs) +- It should be possible for any plugin to append relevant metadata to existing content in search. (e.g. location (path) for TechDocs page) -- It should be easy to refine search queries (e.g. ranking, scoring, etc.) -- It should be easy to customize the search UI -- It should be easy to add search functionality to any Backstage plugin or +- It should be possible to refine search queries (e.g. ranking, scoring, etc.) +- It should be possible to customize the search UI +- It should be possible to add search functionality to any Backstage plugin or deployment + +Architecture non-goals: + +- At this time, we do not intend to directly support event-driven or incremental + index management. Instead, we'll be focused on scheduled, bulk index + management. diff --git a/docs/features/techdocs/configuring-ci-cd.md b/docs/features/techdocs/configuring-ci-cd.md index 5841a02fa8..a9844ef528 100644 --- a/docs/features/techdocs/configuring-ci-cd.md +++ b/docs/features/techdocs/configuring-ci-cd.md @@ -27,11 +27,15 @@ REPOSITORY_URL='https://github.com/org/repo' git clone $REPOSITORY_URL cd repo +# Install @techdocs/cli, mkdocs and mkdocs plugins +npm install -g @techdocs/cli +pip install mkdocs-techdocs-core==0.* + # Generate -npx @techdocs/cli generate +techdocs-cli generate --no-docker # Publish -npx @techdocs/cli publish --publisher-type awsS3 --storage-name --entity +techdocs-cli publish --publisher-type awsS3 --storage-name --entity ``` That's it! @@ -40,14 +44,16 @@ Take a look at [`techdocs-cli` README](https://github.com/backstage/techdocs-cli) for the complete command reference, details, and options. -## 1. Setup a workflow +## Steps + +### 1. Setup a workflow The TechDocs workflow should trigger on CI when any changes are made in the repository containing the documentation files. You can be specific and configure the workflow to be triggered only when files inside the `docs/` directory or `mkdocs.yml` are changed. -## 2. Prepare step +### 2. Prepare step The first step on the CI is to clone your documentation source repository in a working directory. This is almost always the first step in most CI workflows. @@ -62,7 +68,7 @@ step. Eventually we are trying to do a `git clone `. -## 3. Generate step +### 3. Generate step Install [`npx`](https://www.npmjs.com/package/npx) to use it for running `techdocs-cli`. Or you can install using `npm install -g @techdocs/cli`. @@ -78,7 +84,7 @@ npx @techdocs/cli generate --no-docker --source-dir PATH_TO_REPO --output-dir ./ `PATH_TO_REPO` should be the location in the file path where the prepare step above clones the repository. -## 4. Publish step +### 4. Publish step Depending on your cloud storage provider (AWS, Google Cloud, or Azure), set the necessary authentication environment variables. @@ -96,3 +102,68 @@ npx @techdocs/cli publish --publisher-type --storage-name Defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L19). +[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L19). Referenced by: [post](#post), [alert\$](#alert). @@ -67,7 +67,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [alert\$](#alert). @@ -86,7 +86,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -109,6 +109,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md index 0f5c095825..a662b4cb70 100644 --- a/docs/reference/utility-apis/AppThemeApi.md +++ b/docs/reference/utility-apis/AppThemeApi.md @@ -1,7 +1,7 @@ # AppThemeApi The AppThemeApi type is defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:56](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L56). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:56](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L56). The following Utility API implements this type: [appThemeApiRef](./README.md#apptheme) @@ -81,7 +81,7 @@ export type AppTheme = { Defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:25](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L25). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:25](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L25). Referenced by: [getInstalledThemes](#getinstalledthemes). @@ -92,7 +92,7 @@ export type BackstagePalette = Palette & Palette Defined at -[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L74). +[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L74). Referenced by: [BackstageTheme](#backstagetheme). @@ -107,7 +107,7 @@ export interface BackstageTheme extends Theme { Defined at -[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L81). +[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L81). Referenced by: [AppTheme](#apptheme). @@ -136,7 +136,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [activeThemeId\$](#activethemeid). @@ -155,7 +155,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -170,7 +170,7 @@ export type PageTheme = { Defined at -[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L103). +[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L103). Referenced by: [BackstageTheme](#backstagetheme). @@ -183,7 +183,7 @@ export type PageThemeSelector = { Defined at -[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L77). +[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L77). Referenced by: [BackstageTheme](#backstagetheme). @@ -243,7 +243,7 @@ type PaletteAdditions = { Defined at -[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L23). +[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L23). Referenced by: [BackstagePalette](#backstagepalette). @@ -266,6 +266,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md index 69fe5d26ba..80a40d427e 100644 --- a/docs/reference/utility-apis/BackstageIdentityApi.md +++ b/docs/reference/utility-apis/BackstageIdentityApi.md @@ -1,7 +1,7 @@ # BackstageIdentityApi The BackstageIdentityApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L134). +[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L134). The following Utility APIs implement this type: @@ -17,8 +17,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + - [samlAuthApiRef](./README.md#samlauth) ## Members @@ -70,7 +74,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getBackstageIdentity](#getbackstageidentity). @@ -79,7 +83,7 @@ Referenced by: [getBackstageIdentity](#getbackstageidentity). export type BackstageIdentity = { /** - * The Backstage user ID. + * The backstage user ID. */ id: string; @@ -91,6 +95,6 @@ export type BackstageIdentity = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L147). +[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L147). Referenced by: [getBackstageIdentity](#getbackstageidentity). diff --git a/docs/reference/utility-apis/Config.md b/docs/reference/utility-apis/Config.md index 2ae047a700..c5b20610e7 100644 --- a/docs/reference/utility-apis/Config.md +++ b/docs/reference/utility-apis/Config.md @@ -1,7 +1,7 @@ # Config The Config type is defined at -[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L32). The following Utility API implements this type: [configApiRef](./README.md#config) @@ -140,7 +140,7 @@ export type Config = { Defined at -[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L32). Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig), [getConfigArray](#getconfigarray), @@ -153,7 +153,7 @@ export type JsonArray = JsonValue[] Defined at -[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L18). +[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L18). Referenced by: [JsonValue](#jsonvalue). @@ -164,7 +164,7 @@ export type JsonObject = { [key in string]?: JsonValue Defined at -[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L17). +[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L17). Referenced by: [JsonValue](#jsonvalue). @@ -181,7 +181,7 @@ export type JsonValue = Defined at -[packages/config/src/types.ts:19](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L19). +[packages/config/src/types.ts:19](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L19). Referenced by: [get](#get), [getOptional](#getoptional), [JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config). diff --git a/docs/reference/utility-apis/DiscoveryApi.md b/docs/reference/utility-apis/DiscoveryApi.md index ac278fdd97..0d888c1daa 100644 --- a/docs/reference/utility-apis/DiscoveryApi.md +++ b/docs/reference/utility-apis/DiscoveryApi.md @@ -1,7 +1,7 @@ # DiscoveryApi The DiscoveryApi type is defined at -[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30). +[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30). The following Utility API implements this type: [discoveryApiRef](./README.md#discovery) @@ -12,7 +12,7 @@ The following Utility API implements this type: Returns the HTTP base backend URL for a given plugin, without a trailing slash. -This method must always be called just before making a request. as opposed to +This method must always be called just before making a request, as opposed to fetching the URL when constructing an API client. That is to ensure that more flexible routing patterns can be supported. diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md index 9bba0c76c6..1aaecdb47e 100644 --- a/docs/reference/utility-apis/ErrorApi.md +++ b/docs/reference/utility-apis/ErrorApi.md @@ -1,7 +1,7 @@ # ErrorApi The ErrorApi type is defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). +[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). The following Utility API implements this type: [errorApiRef](./README.md#error) @@ -41,7 +41,7 @@ type Error = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). +[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). Referenced by: [post](#post), [error\$](#error). @@ -58,7 +58,7 @@ export type ErrorContext = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). +[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). Referenced by: [post](#post), [error\$](#error). @@ -87,7 +87,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [error\$](#error). @@ -106,7 +106,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -129,6 +129,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/FeatureFlagsApi.md b/docs/reference/utility-apis/FeatureFlagsApi.md index 529d2ac5dc..1e8979c02e 100644 --- a/docs/reference/utility-apis/FeatureFlagsApi.md +++ b/docs/reference/utility-apis/FeatureFlagsApi.md @@ -1,7 +1,7 @@ # FeatureFlagsApi The FeatureFlagsApi type is defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:60](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L60). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:60](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L60). The following Utility API implements this type: [featureFlagsApiRef](./README.md#featureflags) @@ -68,7 +68,7 @@ export type FeatureFlag = { Defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L31). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L31). Referenced by: [registerFlag](#registerflag), [getRegisteredFlags](#getregisteredflags). @@ -83,7 +83,7 @@ export enum FeatureFlagState { Defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:36](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L36). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:36](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L36). Referenced by: [FeatureFlagsSaveOptions](#featureflagssaveoptions). @@ -108,6 +108,6 @@ export type FeatureFlagsSaveOptions = { Defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:44](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L44). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:44](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L44). Referenced by: [save](#save). diff --git a/docs/reference/utility-apis/IdentityApi.md b/docs/reference/utility-apis/IdentityApi.md index a88aaf0b8a..aa2a4dd9a0 100644 --- a/docs/reference/utility-apis/IdentityApi.md +++ b/docs/reference/utility-apis/IdentityApi.md @@ -1,7 +1,7 @@ # IdentityApi The IdentityApi type is defined at -[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). +[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). The following Utility API implements this type: [identityApiRef](./README.md#identity) @@ -76,6 +76,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L162). +[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L162). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/OAuthApi.md b/docs/reference/utility-apis/OAuthApi.md index 79b55812ef..9af6b3bb53 100644 --- a/docs/reference/utility-apis/OAuthApi.md +++ b/docs/reference/utility-apis/OAuthApi.md @@ -1,7 +1,7 @@ # OAuthApi The OAuthApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L67). +[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L67). The following Utility APIs implement this type: @@ -15,8 +15,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + ## Members ### getAccessToken() @@ -82,7 +86,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getAccessToken](#getaccesstoken). @@ -108,6 +112,6 @@ export type OAuthScope = string | string[] Defined at -[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L38). +[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L38). Referenced by: [getAccessToken](#getaccesstoken). diff --git a/docs/reference/utility-apis/OAuthRequestApi.md b/docs/reference/utility-apis/OAuthRequestApi.md index f1c2311ce9..5f521cf288 100644 --- a/docs/reference/utility-apis/OAuthRequestApi.md +++ b/docs/reference/utility-apis/OAuthRequestApi.md @@ -1,7 +1,7 @@ # OAuthRequestApi The OAuthRequestApi type is defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). The following Utility API implements this type: [oauthRequestApiRef](./README.md#oauthrequest) @@ -73,7 +73,7 @@ export type AuthProvider = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). Referenced by: [AuthRequesterOptions](#authrequesteroptions), [PendingAuthRequest](#pendingauthrequest). @@ -97,7 +97,7 @@ export type AuthRequester<AuthResponse> = ( Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). Referenced by: [createAuthRequester](#createauthrequester). @@ -122,7 +122,7 @@ export type AuthRequesterOptions<AuthResponse> = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). Referenced by: [createAuthRequester](#createauthrequester). @@ -151,7 +151,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [authRequest\$](#authrequest). @@ -170,7 +170,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -205,7 +205,7 @@ export type PendingAuthRequest = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). Referenced by: [authRequest\$](#authrequest). @@ -228,6 +228,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/OpenIdConnectApi.md b/docs/reference/utility-apis/OpenIdConnectApi.md index eaea530708..6d05af6189 100644 --- a/docs/reference/utility-apis/OpenIdConnectApi.md +++ b/docs/reference/utility-apis/OpenIdConnectApi.md @@ -1,7 +1,7 @@ # OpenIdConnectApi The OpenIdConnectApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L99). +[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L99). The following Utility APIs implement this type: @@ -13,8 +13,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + ## Members ### getIdToken() @@ -66,6 +70,6 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getIdToken](#getidtoken). diff --git a/docs/reference/utility-apis/ProfileInfoApi.md b/docs/reference/utility-apis/ProfileInfoApi.md index 76d04045e8..1a2f94d031 100644 --- a/docs/reference/utility-apis/ProfileInfoApi.md +++ b/docs/reference/utility-apis/ProfileInfoApi.md @@ -1,7 +1,7 @@ # ProfileInfoApi The ProfileInfoApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L117). +[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L117). The following Utility APIs implement this type: @@ -17,8 +17,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + - [samlAuthApiRef](./README.md#samlauth) ## Members @@ -67,7 +71,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getProfile](#getprofile). @@ -95,6 +99,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L162). +[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L162). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md index 1ef5b6197e..aefbb4b925 100644 --- a/docs/reference/utility-apis/README.md +++ b/docs/reference/utility-apis/README.md @@ -12,7 +12,7 @@ Used to report alerts and forward them to the app Implemented type: [AlertApi](./AlertApi.md) ApiRef: -[alertApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L41) +[alertApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L41) ### appTheme @@ -21,7 +21,7 @@ API Used to configure the app theme, and enumerate options Implemented type: [AppThemeApi](./AppThemeApi.md) ApiRef: -[appThemeApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80) +[appThemeApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80) ### auth0Auth @@ -32,7 +32,7 @@ Implemented types: [OpenIdConnectApi](./OpenIdConnectApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[auth0AuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L275) +[auth0AuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L275) ### config @@ -41,7 +41,7 @@ Used to access runtime configuration Implemented type: [Config](./Config.md) ApiRef: -[configApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) +[configApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ConfigApi.ts#L25) ### discovery @@ -50,7 +50,7 @@ Provides service discovery of backend plugins Implemented type: [DiscoveryApi](./DiscoveryApi.md) ApiRef: -[discoveryApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) +[discoveryApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) ### error @@ -59,7 +59,7 @@ Used to report errors and forward them to the app Implemented type: [ErrorApi](./ErrorApi.md) ApiRef: -[errorApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) +[errorApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) ### featureFlags @@ -68,7 +68,7 @@ Used to toggle functionality in features across Backstage Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md) ApiRef: -[featureFlagsApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83) +[featureFlagsApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83) ### githubAuth @@ -79,7 +79,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[githubAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L232) +[githubAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L232) ### gitlabAuth @@ -90,7 +90,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L262) +[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L262) ### googleAuth @@ -102,7 +102,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[googleAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L215) +[googleAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L215) ### identity @@ -111,7 +111,7 @@ Provides access to the identity of the signed in user Implemented type: [IdentityApi](./IdentityApi.md) ApiRef: -[identityApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) +[identityApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L53) ### microsoftAuth @@ -123,7 +123,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L289) +[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L289) ### oauth2 @@ -135,7 +135,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[oauth2ApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L303) +[oauth2ApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L303) ### oauthRequest @@ -144,7 +144,19 @@ An API for implementing unified OAuth flows in Backstage Implemented type: [OAuthRequestApi](./OAuthRequestApi.md) ApiRef: -[oauthRequestApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) +[oauthRequestApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) + +### oidcAuth + +Example of how to use oidc custom provider + +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) + +ApiRef: +[oidcAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L317) ### oktaAuth @@ -156,7 +168,19 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[oktaAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L245) +[oktaAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L245) + +### oneloginAuth + +Provides authentication towards OneLogin APIs and identities + +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) + +ApiRef: +[oneloginAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L338) ### samlAuth @@ -166,7 +190,7 @@ Implemented types: [ProfileInfoApi](./ProfileInfoApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[samlAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L317) +[samlAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L331) ### storage @@ -175,4 +199,4 @@ Provides the ability to store data which is unique to the user Implemented type: [StorageApi](./StorageApi.md) ApiRef: -[storageApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L68) +[storageApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L68) diff --git a/docs/reference/utility-apis/SessionApi.md b/docs/reference/utility-apis/SessionApi.md index 7d271558d6..4b584d92c8 100644 --- a/docs/reference/utility-apis/SessionApi.md +++ b/docs/reference/utility-apis/SessionApi.md @@ -1,7 +1,7 @@ # SessionApi The SessionApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L190). +[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L190). The following Utility APIs implement this type: @@ -17,8 +17,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + - [samlAuthApiRef](./README.md#samlauth) ## Members @@ -77,7 +81,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [sessionState\$](#sessionstate). @@ -96,7 +100,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -112,7 +116,7 @@ export enum SessionState { Defined at -[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L182). +[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L182). Referenced by: [sessionState\$](#sessionstate). @@ -135,6 +139,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/StorageApi.md b/docs/reference/utility-apis/StorageApi.md index 6c5595d3d8..3247d28f60 100644 --- a/docs/reference/utility-apis/StorageApi.md +++ b/docs/reference/utility-apis/StorageApi.md @@ -1,7 +1,7 @@ # StorageApi The StorageApi type is defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L31). The following Utility API implements this type: [storageApiRef](./README.md#storage) @@ -79,7 +79,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [observe\$](#observe), [StorageApi](#storageapi). @@ -98,7 +98,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -144,7 +144,7 @@ export interface StorageApi { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L31). Referenced by: [forBucket](#forbucket). @@ -158,7 +158,7 @@ export type StorageValueChange<T = any> = { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L21). +[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L21). Referenced by: [observe\$](#observe), [StorageApi](#storageapi). @@ -181,6 +181,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index c947c88e66..c54e0101d3 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1115,6 +1115,10 @@ code { font-size: 115%; } +.post pre { + color: #e4e4e4; +} + .Block__GIF { width: 190px; height: auto; diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index fcd9b795a4..6190dda9da 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,91 @@ # example-app +## 0.2.15 + +### Patch Changes + +- 07bafa248: Add configurable `scope` for oauth2 auth provider. + + Some OAuth2 providers require certain scopes to facilitate a user sign-in using the Authorization Code flow. + This change adds the optional `scope` key to auth.providers.oauth2. An example is: + + ```yaml + auth: + providers: + oauth2: + development: + clientId: + $env: DEV_OAUTH2_CLIENT_ID + clientSecret: + $env: DEV_OAUTH2_CLIENT_SECRET + authorizationUrl: + $env: DEV_OAUTH2_AUTH_URL + tokenUrl: + $env: DEV_OAUTH2_TOKEN_URL + scope: saml-login-selector openid profile email + ``` + + This tells the OAuth 2.0 AS to perform a SAML login and return OIDC information include the `profile` + and `email` claims as part of the ID Token. + +- Updated dependencies [753bb4c40] +- Updated dependencies [6ed2b47d6] +- Updated dependencies [b33fa4cf4] +- Updated dependencies [d36660721] +- Updated dependencies [6b26c9f41] +- Updated dependencies [b3f0c3811] +- Updated dependencies [302795d10] +- Updated dependencies [9ec66c345] +- Updated dependencies [53d3e2d62] +- Updated dependencies [ca559171b] +- Updated dependencies [53348f0af] +- Updated dependencies [f5e564cd6] +- Updated dependencies [68dd79d83] +- Updated dependencies [29a138636] +- Updated dependencies [14aef4b94] +- Updated dependencies [41af18227] +- Updated dependencies [1df75733e] +- Updated dependencies [02d6803e8] +- Updated dependencies [b288a291e] +- Updated dependencies [025c0c7bf] +- Updated dependencies [e5da858d7] +- Updated dependencies [9230d07e7] +- Updated dependencies [f5f45744e] +- Updated dependencies [0fe8ff5be] +- Updated dependencies [c5ab91ce3] +- Updated dependencies [64b9efac2] +- Updated dependencies [19d354c78] +- Updated dependencies [7716d1d70] +- Updated dependencies [8f3443427] +- Updated dependencies [b51ee6ece] +- Updated dependencies [accdfeb30] +- Updated dependencies [804502a5c] + - @backstage/plugin-catalog-import@0.4.0 + - @backstage/plugin-catalog@0.3.1 + - @backstage/plugin-kubernetes@0.3.9 + - @backstage/plugin-rollbar@0.3.0 + - @backstage/plugin-scaffolder@0.5.0 + - @backstage/plugin-cost-insights@0.8.1 + - @backstage/plugin-circleci@0.2.8 + - @backstage/plugin-search@0.3.0 + - @backstage/plugin-cloudbuild@0.2.9 + - @backstage/plugin-register-component@0.2.9 + - @backstage/plugin-sentry@0.3.5 + - @backstage/plugin-jenkins@0.3.9 + - @backstage/plugin-api-docs@0.4.5 + - @backstage/plugin-lighthouse@0.2.10 + - @backstage/plugin-techdocs@0.5.6 + - @backstage/plugin-pagerduty@0.2.8 + - @backstage/plugin-org@0.3.6 + - @backstage/plugin-github-actions@0.3.2 + - @backstage/plugin-explore@0.2.5 + - @backstage/plugin-newrelic@0.2.5 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/plugin-kafka@0.2.2 + - @backstage/core@0.6.1 + - @backstage/plugin-gitops-profiles@0.2.5 + - @backstage/plugin-tech-radar@0.3.5 + ## 0.2.14 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 859372b1f8..88d039e7c8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,38 +1,38 @@ { "name": "example-app", - "version": "0.2.14", + "version": "0.2.15", "private": true, "bundled": true, "dependencies": { "@backstage/catalog-model": "^0.7.1", "@backstage/cli": "^0.6.0", - "@backstage/core": "^0.6.0", - "@backstage/plugin-api-docs": "^0.4.4", - "@backstage/plugin-catalog": "^0.3.0", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/plugin-catalog-import": "^0.3.7", - "@backstage/plugin-circleci": "^0.2.7", - "@backstage/plugin-cloudbuild": "^0.2.8", - "@backstage/plugin-cost-insights": "^0.8.0", - "@backstage/plugin-explore": "^0.2.4", + "@backstage/core": "^0.6.1", + "@backstage/plugin-api-docs": "^0.4.5", + "@backstage/plugin-catalog": "^0.3.1", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/plugin-catalog-import": "^0.4.0", + "@backstage/plugin-circleci": "^0.2.8", + "@backstage/plugin-cloudbuild": "^0.2.9", + "@backstage/plugin-cost-insights": "^0.8.1", + "@backstage/plugin-explore": "^0.2.5", "@backstage/plugin-gcp-projects": "^0.2.4", - "@backstage/plugin-github-actions": "^0.3.1", - "@backstage/plugin-gitops-profiles": "^0.2.4", + "@backstage/plugin-github-actions": "^0.3.2", + "@backstage/plugin-gitops-profiles": "^0.2.5", "@backstage/plugin-graphiql": "^0.2.7", - "@backstage/plugin-org": "^0.3.5", - "@backstage/plugin-jenkins": "^0.3.8", - "@backstage/plugin-kafka": "^0.2.1", - "@backstage/plugin-kubernetes": "^0.3.8", - "@backstage/plugin-lighthouse": "^0.2.9", - "@backstage/plugin-newrelic": "^0.2.4", - "@backstage/plugin-pagerduty": "0.2.7", - "@backstage/plugin-register-component": "^0.2.8", - "@backstage/plugin-rollbar": "^0.2.9", - "@backstage/plugin-scaffolder": "^0.4.2", - "@backstage/plugin-sentry": "^0.3.4", - "@backstage/plugin-search": "^0.2.7", - "@backstage/plugin-tech-radar": "^0.3.4", - "@backstage/plugin-techdocs": "^0.5.5", + "@backstage/plugin-org": "^0.3.6", + "@backstage/plugin-jenkins": "^0.3.9", + "@backstage/plugin-kafka": "^0.2.2", + "@backstage/plugin-kubernetes": "^0.3.9", + "@backstage/plugin-lighthouse": "^0.2.10", + "@backstage/plugin-newrelic": "^0.2.5", + "@backstage/plugin-pagerduty": "0.2.8", + "@backstage/plugin-register-component": "^0.2.9", + "@backstage/plugin-rollbar": "^0.3.0", + "@backstage/plugin-scaffolder": "^0.5.0", + "@backstage/plugin-sentry": "^0.3.5", + "@backstage/plugin-search": "^0.3.0", + "@backstage/plugin-tech-radar": "^0.3.5", + "@backstage/plugin-techdocs": "^0.5.6", "@backstage/plugin-user-settings": "^0.2.5", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -53,7 +53,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 00696736d1..70fceb8a01 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -23,7 +23,7 @@ import { SignInPage, } from '@backstage/core'; import { Router as CatalogRouter } from '@backstage/plugin-catalog'; -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { ExplorePage } from '@backstage/plugin-explore'; import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; @@ -69,10 +69,7 @@ const catalogRouteRef = createRouteRef({ const routes = ( - } - /> + } /> } diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 5474afa72c..e849667c19 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -28,7 +28,11 @@ import { ProvidedApisCard, ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; -import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog'; +import { + AboutCard, + EntityLinksCard, + EntityPageLayout, +} from '@backstage/plugin-catalog'; import { useEntity } from '@backstage/plugin-catalog-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, @@ -177,6 +181,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( )} + + + {isGitHubAvailable(entity) && ( <> diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 01828b1405..6f45f9ba32 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -22,6 +22,7 @@ import { samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, + oauth2ApiRef, oidcAuthApiRef, } from '@backstage/core'; @@ -32,6 +33,12 @@ export const providers = [ message: 'Sign In using OpenId Connect', apiRef: oidcAuthApiRef, }, + { + id: 'oauth2-auth-provider', + title: 'OAuth 2.0', + message: 'Sign In using OAuth 2.0', + apiRef: oauth2ApiRef, + }, { id: 'google-auth-provider', title: 'Google', diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index cc33a7e81b..42f1fa4a42 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-common +## 0.5.3 + +### Patch Changes + +- ffffea8e6: Minor updates to reflect the changes in `@backstage/integration` that made the fields `apiBaseUrl` and `apiUrl` mandatory. +- 82b2c11b6: Set explicit content-type in error handler responses. +- 965e200c6: Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around. +- 5a5163519: Implement `UrlReader.search` which implements glob matching. +- Updated dependencies [ffffea8e6] + - @backstage/integration@0.4.0 + ## 0.5.2 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 895d241c6a..7b1c6870ab 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -32,7 +32,7 @@ "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.5.1", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@octokit/rest": "^18.0.12", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", @@ -69,7 +69,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.7", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index b18e7a4294..5b0b4002a3 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -36,6 +36,7 @@ const gitlabProcessor = new GitlabUrlReader( { host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', }, { treeResponseFactory }, ); @@ -44,6 +45,7 @@ const hostedGitlabProcessor = new GitlabUrlReader( { host: 'gitlab.mycompany.com', apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', + baseUrl: 'https://gitlab.mycompany.com', }, { treeResponseFactory }, ); @@ -379,17 +381,5 @@ describe('GitlabUrlReader', () => { }; await expect(fnGithub).rejects.toThrow(NotFoundError); }); - - it('should throw error when apiBaseUrl is missing', () => { - expect(() => { - /* eslint-disable no-new */ - new GitlabUrlReader( - { - host: 'gitlab.mycompany.com', - }, - { treeResponseFactory }, - ); - }).toThrowError('must configure an explicit apiBaseUrl'); - }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 5e02a9b8e6..66030e30d9 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -52,12 +52,6 @@ export class GitlabUrlReader implements UrlReader { deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { this.treeResponseFactory = deps.treeResponseFactory; - - if (!config.apiBaseUrl) { - throw new Error( - `GitLab integration for '${config.host}' must configure an explicit apiBaseUrl`, - ); - } } async read(url: string): Promise { diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 9c5ac20fa3..07da975184 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -161,7 +161,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(cors(corsOptions)); } app.use(compression()); - app.use(requestLoggingHandler()); + app.use(requestLoggingHandler(logger)); for (const [root, route] of this.routers) { app.use(root, route); } diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index f85c22784d..05549b2085 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,34 @@ # example-backend +## 0.2.15 + +### Patch Changes + +- Updated dependencies [1deb31141] +- Updated dependencies [6ed2b47d6] +- Updated dependencies [77ad0003a] +- Updated dependencies [d2441aee3] +- Updated dependencies [727f0deec] +- Updated dependencies [fb53eb7cb] +- Updated dependencies [07bafa248] +- Updated dependencies [ffffea8e6] +- Updated dependencies [f3fbfb452] +- Updated dependencies [615103a63] +- Updated dependencies [84364b35c] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [5a5163519] +- Updated dependencies [82b2c11b6] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] + - @backstage/plugin-auth-backend@0.3.0 + - @backstage/plugin-scaffolder-backend@0.7.0 + - @backstage/plugin-catalog-backend@0.6.1 + - @backstage/plugin-app-backend@0.3.7 + - example-app@0.2.15 + - @backstage/backend-common@0.5.3 + - @backstage/plugin-techdocs-backend@0.6.0 + ## 0.2.14 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 544bb976fe..e5271f0109 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.14", + "version": "0.2.15", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,24 +27,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/plugin-app-backend": "^0.3.6", - "@backstage/plugin-auth-backend": "^0.2.12", - "@backstage/plugin-catalog-backend": "^0.6.0", + "@backstage/plugin-app-backend": "^0.3.7", + "@backstage/plugin-auth-backend": "^0.3.0", + "@backstage/plugin-catalog-backend": "^0.6.1", "@backstage/plugin-graphql-backend": "^0.1.5", "@backstage/plugin-kubernetes-backend": "^0.2.6", "@backstage/plugin-kafka-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder-backend": "^0.6.0", - "@backstage/plugin-techdocs-backend": "^0.5.5", + "@backstage/plugin-scaffolder-backend": "^0.7.0", + "@backstage/plugin-techdocs-backend": "^0.6.0", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.14", + "example-app": "^0.2.15", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index c8042b72e9..87d6e2cff0 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/catalog-client +## 0.3.6 + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- 72b96e880: Add the `presence` argument to the `CatalogApi` to be able to register optional locations. + ## 0.3.5 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 00b05e37bf..1d0e890f4e 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 6369f95b76..19da45e4ec 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -21,6 +21,7 @@ import { CatalogClient } from './CatalogClient'; import { CatalogListResponse, DiscoveryApi } from './types'; const server = setupServer(); +const token = 'fake-token'; const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discoveryApi: DiscoveryApi = { async getBaseUrl(_pluginId) { @@ -71,7 +72,7 @@ describe('CatalogClient', () => { }); it('should entities from correct endpoint', async () => { - const response = await client.getEntities(); + const response = await client.getEntities({}, { token }); expect(response).toEqual(defaultResponse); }); @@ -85,13 +86,16 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ - filter: { - a: '1', - b: ['2', '3'], - ö: '=', + const response = await client.getEntities( + { + filter: { + a: '1', + b: ['2', '3'], + ö: '=', + }, }, - }); + { token }, + ); expect(response.items).toEqual([]); }); @@ -106,11 +110,61 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ - fields: ['a.b', 'ö'], - }); + const response = await client.getEntities( + { + fields: ['a.b', 'ö'], + }, + { token }, + ); expect(response.items).toEqual([]); }); }); + + describe('getLocationById', () => { + const defaultResponse = { + data: { + id: '42', + }, + }; + + beforeEach(() => { + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (_, res, ctx) => { + return res(ctx.json(defaultResponse)); + }), + ); + }); + + it('should locations from correct endpoint', async () => { + const response = await client.getLocationById('42', { token }); + expect(response).toEqual(defaultResponse); + }); + + it('forwards authorization token', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe(`Bearer ${token}`); + return res(ctx.json(defaultResponse)); + }), + ); + + await client.getLocationById('42', { token }); + }); + + it('skips authorization header if token is omitted', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toBeNull(); + return res(ctx.json(defaultResponse)); + }), + ); + + await client.getLocationById('42'); + }); + }); }); diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 362b1e71da..04206edcdb 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -24,6 +24,7 @@ import fetch from 'cross-fetch'; import { AddLocationRequest, AddLocationResponse, + CatalogRequestOptions, CatalogApi, CatalogEntitiesRequest, CatalogListResponse, @@ -37,12 +38,16 @@ export class CatalogClient implements CatalogApi { this.discoveryApi = options.discoveryApi; } - async getLocationById(id: String): Promise { - return await this.getOptional(`/locations/${id}`); + async getLocationById( + id: String, + options?: CatalogRequestOptions, + ): Promise { + return await this.getOptional(`/locations/${id}`, options); } async getEntities( request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, ): Promise> { const { filter = {}, fields = [] } = request ?? {}; const params: string[] = []; @@ -62,20 +67,28 @@ export class CatalogClient implements CatalogApi { } const query = params.length ? `?${params.join('&')}` : ''; - const entities: Entity[] = await this.getRequired(`/entities${query}`); + const entities: Entity[] = await this.getRequired( + `/entities${query}`, + options, + ); return { items: entities }; } - async getEntityByName(compoundName: EntityName): Promise { + async getEntityByName( + compoundName: EntityName, + options?: CatalogRequestOptions, + ): Promise { const { kind, namespace = 'default', name } = compoundName; - return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`); + return this.getOptional( + `/entities/by-name/${kind}/${namespace}/${name}`, + options, + ); } - async addLocation({ - type = 'url', - target, - dryRun, - }: AddLocationRequest): Promise { + async addLocation( + { type = 'url', target, dryRun, presence }: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' @@ -83,9 +96,10 @@ export class CatalogClient implements CatalogApi { { headers: { 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), }, method: 'POST', - body: JSON.stringify({ type, target }), + body: JSON.stringify({ type, target, presence }), }, ); @@ -110,18 +124,30 @@ export class CatalogClient implements CatalogApi { }; } - async getLocationByEntity(entity: Entity): Promise { + async getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise { const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - const all: { data: Location }[] = await this.getRequired('/locations'); + const all: { data: Location }[] = await this.getRequired( + '/locations', + options, + ); return all .map(r => r.data) .find(l => locationCompound === `${l.type}:${l.target}`); } - async removeEntityByUid(uid: string): Promise { + async removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { + headers: options?.token + ? { Authorization: `Bearer ${options.token}` } + : {}, method: 'DELETE', }, ); @@ -138,9 +164,16 @@ export class CatalogClient implements CatalogApi { // Private methods // - private async getRequired(path: string): Promise { + private async getRequired( + path: string, + options?: CatalogRequestOptions, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const response = await fetch(url); + const response = await fetch(url, { + headers: options?.token + ? { Authorization: `Bearer ${options.token}` } + : {}, + }); if (!response.ok) { const payload = await response.text(); @@ -151,9 +184,16 @@ export class CatalogClient implements CatalogApi { return await response.json(); } - private async getOptional(path: string): Promise { + private async getOptional( + path: string, + options?: CatalogRequestOptions, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const response = await fetch(url); + const response = await fetch(url, { + headers: options?.token + ? { Authorization: `Bearer ${options.token}` } + : {}, + }); if (!response.ok) { if (response.status === 404) { diff --git a/packages/catalog-client/src/index.ts b/packages/catalog-client/src/index.ts index 9bd13f9b7c..c5a626e25b 100644 --- a/packages/catalog-client/src/index.ts +++ b/packages/catalog-client/src/index.ts @@ -15,4 +15,10 @@ */ export { CatalogClient } from './CatalogClient'; -export type { CatalogApi } from './types'; +export type { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, +} from './types'; diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index e72317d444..e460843657 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -25,21 +25,42 @@ export type CatalogListResponse = { items: T[]; }; +export type CatalogRequestOptions = { + token?: string; +}; + export interface CatalogApi { - getLocationById(id: String): Promise; - getEntityByName(name: EntityName): Promise; + getLocationById( + id: String, + options?: CatalogRequestOptions, + ): Promise; + getEntityByName( + name: EntityName, + options?: CatalogRequestOptions, + ): Promise; getEntities( request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, ): Promise>; - addLocation(location: AddLocationRequest): Promise; - getLocationByEntity(entity: Entity): Promise; - removeEntityByUid(uid: string): Promise; + addLocation( + location: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise; + getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise; + removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise; } export type AddLocationRequest = { type?: string; target: string; dryRun?: boolean; + presence?: 'optional' | 'required'; }; export type AddLocationResponse = { diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index fbf28d7fcb..29c892274d 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -7,11 +7,24 @@ metadata: - java - data links: - - url: https://example.com/apm/artists-lookup - title: APM + - url: https://example.com/user + title: Examples Users + icon: user + - url: https://example.com/group + title: Example Group + icon: group + - url: https://example.com/cloud + title: Link with Cloud Icon + icon: cloud + - url: https://example.com/dashboard + title: Dashboard icon: dashboard - - url: https://example.com/logs/artists-lookup - title: Logs + - url: https://example.com/help + title: Support + icon: help + - url: https://example.com/web + title: Website + icon: web spec: type: service lifecycle: experimental diff --git a/packages/cli/package.json b/packages/cli/package.json index 9aaeb00459..dde6bc34ba 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,7 +43,7 @@ "@spotify/eslint-config-react": "^9.0.0", "@spotify/eslint-config-typescript": "^9.0.0", "@sucrase/webpack-loader": "^2.0.0", - "@svgr/plugin-jsx": "5.4.x", + "@svgr/plugin-jsx": "5.5.x", "@svgr/plugin-svgo": "5.4.x", "@svgr/rollup": "5.5.x", "@svgr/webpack": "5.4.x", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 60a6e43654..a5a56f633a 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core +## 0.6.1 + +### Patch Changes + +- b51ee6ece: Fixed type inference of `createRouteRef`. + ## 0.6.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 41dbc8ef14..c9045c6465 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.6.0", + "version": "0.6.1", "private": false, "publishConfig": { "access": "public", @@ -67,7 +67,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 77eba9759a..28056b33bf 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -31,6 +31,7 @@ export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './Progress'; export * from './ProgressBars'; +export * from './Select'; export * from './SimpleStepper'; export * from './Status'; export * from './StructuredMetadataTable'; diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 57cf435651..e41a45a225 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -215,7 +215,6 @@ export const SidebarItem = forwardRef((props, ref) => { {...childProps} activeClassName={classes.selected} to={props.to} - end ref={ref} > {content} diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 13b854972a..6a0f75ebaf 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,101 @@ # @backstage/create-app +## 0.3.9 + +### Patch Changes + +- 615103a63: Pass on plugin database management instance that is now required by the scaffolder plugin. + + To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`: + + ```diff + export default async function createPlugin({ + logger, + config, + + database, + }: PluginEnvironment) { + + // ...omitted... + + return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + dockerClient, + entityClient, + + database, + }); + } + ``` + +- 30e200d12: `@backstage/plugin-catalog-import` has been refactored, so the `App.tsx` of the backstage apps need to be updated: + + ```diff + // packages/app/src/App.tsx + + } + + element={} + /> + ``` + +- f4b576d0e: TechDocs: Add comments about migrating away from basic setup in app-config.yaml +- Updated dependencies [753bb4c40] +- Updated dependencies [1deb31141] +- Updated dependencies [6ed2b47d6] +- Updated dependencies [77ad0003a] +- Updated dependencies [6b26c9f41] +- Updated dependencies [b3f0c3811] +- Updated dependencies [d2441aee3] +- Updated dependencies [727f0deec] +- Updated dependencies [fb53eb7cb] +- Updated dependencies [07bafa248] +- Updated dependencies [ca559171b] +- Updated dependencies [ffffea8e6] +- Updated dependencies [f5e564cd6] +- Updated dependencies [f3fbfb452] +- Updated dependencies [615103a63] +- Updated dependencies [68dd79d83] +- Updated dependencies [84364b35c] +- Updated dependencies [41af18227] +- Updated dependencies [82b2c11b6] +- Updated dependencies [1df75733e] +- Updated dependencies [965e200c6] +- Updated dependencies [b51ee6ece] +- Updated dependencies [e5da858d7] +- Updated dependencies [9230d07e7] +- Updated dependencies [f5f45744e] +- Updated dependencies [0fe8ff5be] +- Updated dependencies [5a5163519] +- Updated dependencies [82b2c11b6] +- Updated dependencies [8f3443427] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] +- Updated dependencies [b51ee6ece] +- Updated dependencies [804502a5c] + - @backstage/plugin-catalog-import@0.4.0 + - @backstage/plugin-auth-backend@0.3.0 + - @backstage/plugin-catalog@0.3.1 + - @backstage/plugin-scaffolder@0.5.0 + - @backstage/plugin-scaffolder-backend@0.7.0 + - @backstage/plugin-catalog-backend@0.6.1 + - @backstage/plugin-circleci@0.2.8 + - @backstage/plugin-search@0.3.0 + - @backstage/plugin-app-backend@0.3.7 + - @backstage/backend-common@0.5.3 + - @backstage/plugin-api-docs@0.4.5 + - @backstage/plugin-lighthouse@0.2.10 + - @backstage/plugin-techdocs@0.5.6 + - @backstage/test-utils@0.1.7 + - @backstage/plugin-github-actions@0.3.2 + - @backstage/plugin-explore@0.2.5 + - @backstage/plugin-techdocs-backend@0.6.0 + - @backstage/core@0.6.1 + - @backstage/plugin-tech-radar@0.3.5 + ## 0.3.8 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 5f32a5d13d..5e4cf8ae11 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.8", + "version": "0.3.9", "private": false, "publishConfig": { "access": "public" @@ -44,31 +44,31 @@ "ts-node": "^8.6.2" }, "peerDependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/cli": "^0.6.0", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", - "@backstage/plugin-api-docs": "^0.4.4", - "@backstage/plugin-app-backend": "^0.3.6", - "@backstage/plugin-auth-backend": "^0.2.12", - "@backstage/plugin-catalog": "^0.3.0", - "@backstage/plugin-catalog-backend": "^0.6.0", - "@backstage/plugin-catalog-import": "^0.3.7", - "@backstage/plugin-circleci": "^0.2.7", - "@backstage/plugin-explore": "^0.2.4", - "@backstage/plugin-github-actions": "^0.3.1", - "@backstage/plugin-lighthouse": "^0.2.9", + "@backstage/core": "^0.6.1", + "@backstage/plugin-api-docs": "^0.4.5", + "@backstage/plugin-app-backend": "^0.3.7", + "@backstage/plugin-auth-backend": "^0.3.0", + "@backstage/plugin-catalog": "^0.3.1", + "@backstage/plugin-catalog-backend": "^0.6.1", + "@backstage/plugin-catalog-import": "^0.4.0", + "@backstage/plugin-circleci": "^0.2.8", + "@backstage/plugin-explore": "^0.2.5", + "@backstage/plugin-github-actions": "^0.3.2", + "@backstage/plugin-lighthouse": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder": "^0.4.2", - "@backstage/plugin-search": "^0.2.7", - "@backstage/plugin-scaffolder-backend": "^0.6.0", - "@backstage/plugin-tech-radar": "^0.3.4", - "@backstage/plugin-techdocs": "^0.5.5", - "@backstage/plugin-techdocs-backend": "^0.5.5", + "@backstage/plugin-scaffolder": "^0.5.0", + "@backstage/plugin-search": "^0.3.0", + "@backstage/plugin-scaffolder-backend": "^0.7.0", + "@backstage/plugin-tech-radar": "^0.3.5", + "@backstage/plugin-techdocs": "^0.5.6", + "@backstage/plugin-techdocs-backend": "^0.6.0", "@backstage/plugin-user-settings": "^0.2.5", - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@backstage/theme": "^0.2.3" }, "nodemonConfig": { diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 495b30dd6f..f8a345cd1e 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -57,6 +57,9 @@ proxy: changeOrigin: true # Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach techdocs: builder: 'local' # Alternatives - 'external' generators: diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 6deedcd748..711e501224 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -4,7 +4,6 @@ import { AlertDisplay, OAuthRequestDialog, SidebarPage, - createRouteRef, FlatRoutes, } from '@backstage/core'; import { apis } from './apis'; @@ -13,7 +12,7 @@ import { AppSidebar } from './sidebar'; import { Route, Navigate } from 'react-router'; import { Router as CatalogRouter } from '@backstage/plugin-catalog'; import { Router as DocsRouter } from '@backstage/plugin-techdocs'; -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { SearchPage as SearchRouter } from '@backstage/plugin-search'; import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; @@ -29,12 +28,6 @@ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); const deprecatedAppRoutes = app.getRoutes(); -const catalogRouteRef = createRouteRef({ - path: '/catalog', - title: 'Service Catalog', -}); - - const App = () => ( @@ -53,10 +46,7 @@ const App = () => ( path="/tech-radar" element={} /> - } - /> + } /> } diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 251a4db7ee..4e16deebdc 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/dev-utils +## 0.1.10 + +### Patch Changes + +- Updated dependencies [b51ee6ece] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/test-utils@0.1.7 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.1.9 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 3a01306b46..7f53d029b3 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.9", + "version": "0.1.10", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/test-utils": "^0.1.5", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/test-utils": "^0.1.7", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index ce2592e03b..7c8a092081 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,6 +1,5 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], - ignorePatterns: ['templates/**'], rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 11839d733b..7056976458 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/integration +## 0.4.0 + +### Minor Changes + +- ffffea8e6: Update the `GitLabIntegrationConfig` to require the fields `apiBaseUrl` and `baseUrl`. The `readGitLabIntegrationConfig` function is now more strict and has better error reporting. This change mirrors actual reality in code more properly - the fields are actually necessary for many parts of code to actually function, so they should no longer be optional. + + Some checks that used to happen deep inside code that consumed config, now happen upfront at startup. This means that you may encounter new errors at backend startup, if you had actual mistakes in config but didn't happen to exercise the code paths that actually would break. But for most users, no change will be necessary. + + An example minimal GitLab config block that just adds a token to public GitLab would look similar to this: + + ```yaml + integrations: + gitlab: + - host: gitlab.com + token: + $env: GITLAB_TOKEN + ``` + + A full fledged config that points to a locally hosted GitLab could look like this: + + ```yaml + integrations: + gitlab: + - host: gitlab.my-company.com + apiBaseUrl: https://gitlab.my-company.com/api/v4 + baseUrl: https://gitlab.my-company.com + token: + $env: OUR_GITLAB_TOKEN + ``` + + In this case, the only optional field is `baseUrl` which is formed from the `host` if needed. + ## 0.3.2 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index f82472ca97..b4134ab00e 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.3.2", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,7 +38,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.7", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", "msw": "^0.21.2" diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 8814e33302..6faae3a5e7 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -26,6 +26,8 @@ describe('GitLabIntegration', () => { { host: 'h.com', token: 't', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', }, ], }, diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index 303b4bd270..bfa59d001e 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -31,6 +31,7 @@ describe('readGitLabIntegrationConfig', () => { buildConfig({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', }), ); @@ -38,12 +39,15 @@ describe('readGitLabIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', }); }); it('inserts the defaults if missing', () => { - const output = readGitLabIntegrationConfig(buildConfig({})); + const output = readGitLabIntegrationConfig( + buildConfig({ host: 'gitlab.com' }), + ); expect(output).toEqual({ host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', @@ -88,12 +92,15 @@ describe('readGitLabIntegrationConfigs', () => { { host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com/api/v4', + baseUrl: 'https://a.com', }, ]), ); expect(output).toContainEqual({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com/api/v4', baseUrl: 'https://a.com', }); }); @@ -104,6 +111,7 @@ describe('readGitLabIntegrationConfigs', () => { { host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', }, ]); }); diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 47269b4c32..cf717e08d2 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { isValidHost } from '../helpers'; +import { isValidHost, isValidUrl } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; @@ -38,7 +38,7 @@ export type GitLabIntegrationConfig = { * The API will always be preferred if both its base URL and a token are * present. */ - apiBaseUrl?: string; + apiBaseUrl: string; /** * The authorization token to use for requests this provider. @@ -53,7 +53,7 @@ export type GitLabIntegrationConfig = { * * If no baseUrl is provided, it will default to https://${host} */ - baseUrl?: string; + baseUrl: string; }; /** @@ -64,16 +64,10 @@ export type GitLabIntegrationConfig = { export function readGitLabIntegrationConfig( config: Config, ): GitLabIntegrationConfig { - const host = config.getOptionalString('host') ?? GITLAB_HOST; + const host = config.getString('host'); let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const token = config.getOptionalString('token'); - const baseUrl = config.getOptionalString('baseUrl') ?? `https://${host}`; - - if (!isValidHost(host)) { - throw new Error( - `Invalid GitLab integration config, '${host}' is not a valid host`, - ); - } + let baseUrl = config.getOptionalString('baseUrl'); if (apiBaseUrl) { apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); @@ -81,6 +75,30 @@ export function readGitLabIntegrationConfig( apiBaseUrl = GITLAB_API_BASE_URL; } + if (baseUrl) { + baseUrl = baseUrl.replace(/\/+$/, ''); + } else { + baseUrl = `https://${host}`; + } + + if (host.includes(':')) { + throw new Error( + `Invalid GitLab integration config, host '${host}' should just be the host name (e.g. "github.com"), not a URL`, + ); + } else if (!isValidHost(host)) { + throw new Error( + `Invalid GitLab integration config, '${host}' is not a valid host`, + ); + } else if (!apiBaseUrl || !isValidUrl(apiBaseUrl)) { + throw new Error( + `Invalid GitLab integration config, '${apiBaseUrl}' is not a valid apiBaseUrl`, + ); + } else if (!isValidUrl(baseUrl)) { + throw new Error( + `Invalid GitLab integration config, '${baseUrl}' is not a valid baseUrl`, + ); + } + return { host, token, apiBaseUrl, baseUrl }; } @@ -99,7 +117,11 @@ export function readGitLabIntegrationConfigs( // As a convenience we always make sure there's at least an unauthenticated // reader for public gitlab repos. if (!result.some(c => c.host === GITLAB_HOST)) { - result.push({ host: GITLAB_HOST, apiBaseUrl: GITLAB_API_BASE_URL }); + result.push({ + host: GITLAB_HOST, + apiBaseUrl: GITLAB_API_BASE_URL, + baseUrl: `https://${GITLAB_HOST}`, + }); } return result; diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 2b4713604c..6836854eae 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -37,10 +37,14 @@ describe('gitlab core', () => { const configWithToken: GitLabIntegrationConfig = { host: 'g.com', token: '0123456789', + apiBaseUrl: '', + baseUrl: '', }; const configWithNoToken: GitLabIntegrationConfig = { host: 'g.com', + apiBaseUrl: '', + baseUrl: '', }; describe('getGitLabFileFetchUrl', () => { diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index cc1c59a238..73cef18297 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -16,11 +16,22 @@ import { ScmIntegration, ScmIntegrationsGroup } from './types'; -/** Checks whether the given url is a valid host */ -export function isValidHost(url: string): boolean { +/** Checks whether the given argument is a valid URL hostname */ +export function isValidHost(host: string): boolean { const check = new URL('http://example.com'); - check.host = url; - return check.host === url; + check.host = host; + return check.host === host; +} + +/** Checks whether the given argument is a valid URL */ +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } } export function basicIntegrations( diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index baf7a380a2..9f2a8c0be4 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/techdocs-common +## 0.4.0 + +### Minor Changes + +- 08142b256: URL Preparer will now use proper etag based caching introduced in https://github.com/backstage/backstage/pull/4120. Previously, builds used to be cached for 30 minutes. + +### Patch Changes + +- 77ad0003a: Revert AWS SDK version to v2 +- 08142b256: TechDocs will throw warning in backend logs when legacy git preparer or dir preparer is used to preparer docs. Migrate to URL Preparer by updating `backstage.io/techdocs-ref` annotation to be prefixed with `url:`. + Detailed docs are here https://backstage.io/docs/features/techdocs/how-to-guides#how-to-use-url-reader-in-techdocs-prepare-step + See benefits and reason for doing so https://github.com/backstage/backstage/issues/4409 +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + ## 0.3.7 ### Patch Changes diff --git a/packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts similarity index 52% rename from packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts rename to packages/techdocs-common/__mocks__/aws-sdk.ts index 2d2581a6cd..987b3d2800 100644 --- a/packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -13,49 +13,74 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { S3ClientConfig } from '@aws-sdk/client-s3'; +import type { S3 as S3Types } from 'aws-sdk'; import { EventEmitter } from 'events'; import fs from 'fs'; export class S3 { private readonly options; - constructor(options: S3ClientConfig) { + constructor(options: S3Types.ClientConfiguration) { this.options = options; } headObject({ Key }: { Key: string }) { + return { + promise: () => this.checkFileExists(Key), + }; + } + + getObject({ Key }: { Key: string }) { + return { + promise: () => this.checkFileExists(Key), + createReadStream: () => { + const emitter = new EventEmitter(); + process.nextTick(() => { + if (fs.existsSync(Key)) { + emitter.emit('data', Buffer.from(fs.readFileSync(Key))); + } else { + emitter.emit( + 'error', + new Error(`The file ${Key} doest not exist !`), + ); + } + emitter.emit('end'); + }); + return emitter; + }, + }; + } + + checkFileExists(Key: string) { return new Promise((resolve, reject) => { if (fs.existsSync(Key)) { resolve(''); } else { - reject({ message: `The file ${Key} doest not exist.` }); - } - }); - } - - getObject({ Key }: { Key: string }) { - return new Promise((resolve, reject) => { - if (fs.existsSync(Key)) { - const emitter = new EventEmitter(); - process.nextTick(() => { - emitter.emit('data', Buffer.from(fs.readFileSync(Key))); - emitter.emit('end'); - }); - resolve({ - Body: emitter, - }); - } else { - reject({ message: `The file ${Key} doest not exist.` }); + reject({ message: 'The object doest not exist !' }); } }); } headBucket() { - return ''; + return new Promise(resolve => { + resolve(''); + }); } - putObject() { - return ''; + upload({ Key }: { Key: string }) { + return { + promise: () => + new Promise((resolve, reject) => { + if (!fs.existsSync(Key)) { + reject(''); + } else { + resolve(''); + } + }), + }; } } + +export default { + S3, +}; diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 9b0f7fbf9f..f1236cd6d5 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.3.7", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -36,16 +36,16 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@aws-sdk/client-s3": "^3.1.0", "@azure/identity": "^1.2.2", "@azure/storage-blob": "^12.4.0", - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@google-cloud/storage": "^5.6.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", + "aws-sdk": "^2.840.0", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", "express": "^4.17.1", @@ -60,7 +60,6 @@ "winston": "^3.2.1" }, "devDependencies": { - "@aws-sdk/types": "3.1.0", "@backstage/cli": "^0.6.0", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", diff --git a/packages/techdocs-common/src/helpers.test.ts b/packages/techdocs-common/src/helpers.test.ts index eba1386b11..6520f0f8e4 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -139,7 +139,7 @@ describe('getDocFilesFromRepository', () => { archive: async () => { return Readable.from(''); }, - etag: '', + etag: 'etag123abc', }; } @@ -156,6 +156,7 @@ describe('getDocFilesFromRepository', () => { mockEntityWithAnnotation, ); - expect(output).toBe('/tmp/testfolder'); + expect(output.preparedDir).toBe('/tmp/testfolder'); + expect(output.etag).toBe('etag123abc'); }); }); diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index f70b0f3840..beff0b2a6e 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import os from 'os'; -import path from 'path'; -import parseGitUrl from 'git-url-parse'; -import fs from 'fs-extra'; -import { InputError, UrlReader, Git } from '@backstage/backend-common'; +import { Git, InputError, UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import fs from 'fs-extra'; +import parseGitUrl from 'git-url-parse'; +import os from 'os'; +import path from 'path'; +import { Logger } from 'winston'; import { getDefaultBranch } from './default-branch'; import { getGitRepoType, getTokenForGitRepo } from './git-auth'; -import { RemoteProtocol } from './stages/prepare/types'; -import { Logger } from 'winston'; +import { PreparerResponse, RemoteProtocol } from './stages/prepare/types'; export type ParsedLocationAnnotation = { type: RemoteProtocol; @@ -196,16 +196,9 @@ export const checkoutGitRepository = async ( }; export const getLastCommitTimestamp = async ( - repositoryUrl: string, - config: Config, + repositoryLocation: string, logger: Logger, ): Promise => { - const repositoryLocation = await checkoutGitRepository( - repositoryUrl, - config, - logger, - ); - const git = Git.fromAuth({ logger }); const sha = await git.resolveRef({ dir: repositoryLocation, ref: 'HEAD' }); const commit = await git.readCommit({ dir: repositoryLocation, sha }); @@ -216,13 +209,22 @@ export const getLastCommitTimestamp = async ( export const getDocFilesFromRepository = async ( reader: UrlReader, entity: Entity, -): Promise => { + opts?: { etag?: string; logger?: Logger }, +): Promise => { const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); - const response = await reader.readTree(target); + opts?.logger?.info(`Reading files from ${target}`); + // readTree will throw NotModifiedError if etag has not changed. + const readTreeResponse = await reader.readTree(target, { etag: opts?.etag }); + const preparedDir = await readTreeResponse.dir(); - return await response.dir(); + opts?.logger?.info(`Tree downloaded and stored at ${preparedDir}`); + + return { + preparedDir, + etag: readTreeResponse.etag, + }; }; diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.test.ts b/packages/techdocs-common/src/stages/prepare/commonGit.test.ts index 704cd23daa..dcc06313bd 100644 --- a/packages/techdocs-common/src/stages/prepare/commonGit.test.ts +++ b/packages/techdocs-common/src/stages/prepare/commonGit.test.ts @@ -16,8 +16,8 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { CommonGitPreparer } from './commonGit'; import { checkoutGitRepository } from '../../helpers'; +import { CommonGitPreparer } from './commonGit'; function normalizePath(path: string) { return path @@ -29,6 +29,7 @@ function normalizePath(path: string) { jest.mock('../../helpers', () => ({ ...jest.requireActual<{}>('../../helpers'), checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'), + getLastCommitTimestamp: jest.fn(() => 12345678), })); const createMockEntity = (annotations = {}) => { @@ -57,9 +58,9 @@ describe('commonGit preparer', () => { 'github:https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', }); - const tempDocsPath = await preparer.prepare(mockEntity); + const { preparedDir } = await preparer.prepare(mockEntity); expect(checkoutGitRepository).toHaveBeenCalledTimes(1); - expect(normalizePath(tempDocsPath)).toEqual( + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', ); }); @@ -72,9 +73,9 @@ describe('commonGit preparer', () => { 'gitlab:https://gitlab.com/xesjkeee/go-logger/blob/master/catalog-info.yaml', }); - const tempDocsPath = await preparer.prepare(mockEntity); + const { preparedDir } = await preparer.prepare(mockEntity); expect(checkoutGitRepository).toHaveBeenCalledTimes(2); - expect(normalizePath(tempDocsPath)).toEqual( + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/catalog-info.yaml', ); }); @@ -87,9 +88,9 @@ describe('commonGit preparer', () => { 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', }); - const tempDocsPath = await preparer.prepare(mockEntity); + const { preparedDir } = await preparer.prepare(mockEntity); expect(checkoutGitRepository).toHaveBeenCalledTimes(3); - expect(normalizePath(tempDocsPath)).toEqual( + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/template.yaml', ); }); diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.ts b/packages/techdocs-common/src/stages/prepare/commonGit.ts index 7eac07d76f..46b96675f3 100644 --- a/packages/techdocs-common/src/stages/prepare/commonGit.ts +++ b/packages/techdocs-common/src/stages/prepare/commonGit.ts @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import path from 'path'; -import parseGitUrl from 'git-url-parse'; +import { NotModifiedError } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { PreparerBase } from './types'; -import { parseReferenceAnnotation, checkoutGitRepository } from '../../helpers'; - +import parseGitUrl from 'git-url-parse'; +import path from 'path'; import { Logger } from 'winston'; +import { + checkoutGitRepository, + getLastCommitTimestamp, + parseReferenceAnnotation, +} from '../../helpers'; +import { PreparerBase, PreparerResponse } from './types'; export class CommonGitPreparer implements PreparerBase { private readonly config: Config; @@ -31,23 +35,45 @@ export class CommonGitPreparer implements PreparerBase { this.logger = logger; } - async prepare(entity: Entity): Promise { + async prepare( + entity: Entity, + options?: { etag?: string }, + ): Promise { + this.logger.warn( + 'You are using the legacy git preparer in TechDocs which will be removed in near future (March 2021). ' + + 'Migrate to URL reader by updating `backstage.io/techdocs-ref` annotation in `catalog-info.yaml` ' + + 'to be prefixed with `url:`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 ', + ); + const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); try { + // Update repository or do a fresh clone. const repoPath = await checkoutGitRepository( target, this.config, this.logger, ); - const parsedGitLocation = parseGitUrl(target); + // Check if etag has changed for cache invalidation. + const etag = await getLastCommitTimestamp(repoPath, this.logger); + if (options?.etag === etag.toString()) { + throw new NotModifiedError(); + } - return path.join(repoPath, parsedGitLocation.filepath); + const parsedGitLocation = parseGitUrl(target); + return { + preparedDir: path.join(repoPath, parsedGitLocation.filepath), + etag: etag.toString(), + }; } catch (error) { - this.logger.debug(`Repo checkout failed with error ${error.message}`); + if (error instanceof NotModifiedError) { + this.logger.debug(`Cache is valid for etag ${options?.etag}`); + } else { + this.logger.debug(`Repo checkout failed with error ${error.message}`); + } throw error; } } diff --git a/packages/techdocs-common/src/stages/prepare/dir.test.ts b/packages/techdocs-common/src/stages/prepare/dir.test.ts index cd8d358afe..67fa0dd21f 100644 --- a/packages/techdocs-common/src/stages/prepare/dir.test.ts +++ b/packages/techdocs-common/src/stages/prepare/dir.test.ts @@ -15,8 +15,8 @@ */ import { getVoidLogger, UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { DirectoryPreparer } from './dir'; import { checkoutGitRepository } from '../../helpers'; +import { DirectoryPreparer } from './dir'; function normalizePath(path: string) { return path @@ -28,6 +28,7 @@ function normalizePath(path: string) { jest.mock('../../helpers', () => ({ ...jest.requireActual<{}>('../../helpers'), checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'), + getLastCommitTimestamp: jest.fn(() => 12345678), })); const logger = getVoidLogger(); @@ -66,9 +67,8 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:./our-documentation', }); - expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( - '/directory/our-documentation', - ); + const { preparedDir } = await directoryPreparer.prepare(mockEntity); + expect(normalizePath(preparedDir)).toEqual('/directory/our-documentation'); }); it('should merge managed-by-location and techdocs-ref when techdocs-ref is absolute', async () => { @@ -84,9 +84,8 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:/our-documentation/techdocs', }); - expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( - '/our-documentation/techdocs', - ); + const { preparedDir } = await directoryPreparer.prepare(mockEntity); + expect(normalizePath(preparedDir)).toEqual('/our-documentation/techdocs'); }); it('should merge managed-by-location and techdocs-ref when managed-by-location is a git repository', async () => { @@ -102,7 +101,8 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:./docs', }); - expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( + const { preparedDir } = await directoryPreparer.prepare(mockEntity); + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/docs', ); expect(checkoutGitRepository).toHaveBeenCalledTimes(1); diff --git a/packages/techdocs-common/src/stages/prepare/dir.ts b/packages/techdocs-common/src/stages/prepare/dir.ts index ab277fda9a..08832938df 100644 --- a/packages/techdocs-common/src/stages/prepare/dir.ts +++ b/packages/techdocs-common/src/stages/prepare/dir.ts @@ -13,14 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PreparerBase } from './types'; +import { + InputError, + NotModifiedError, + UrlReader, +} from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import path from 'path'; -import { parseReferenceAnnotation, checkoutGitRepository } from '../../helpers'; -import { UrlReader, InputError } from '@backstage/backend-common'; import parseGitUrl from 'git-url-parse'; +import path from 'path'; import { Logger } from 'winston'; +import { + checkoutGitRepository, + getLastCommitTimestamp, + parseReferenceAnnotation, +} from '../../helpers'; +import { PreparerBase, PreparerResponse } from './types'; export class DirectoryPreparer implements PreparerBase { constructor( @@ -33,7 +41,10 @@ export class DirectoryPreparer implements PreparerBase { this.reader = reader; } - private async resolveManagedByLocationToDir(entity: Entity) { + private async resolveManagedByLocationToDir( + entity: Entity, + options?: { etag?: string }, + ): Promise { const { type, target } = parseReferenceAnnotation( 'backstage.io/managed-by-location', entity, @@ -44,8 +55,14 @@ export class DirectoryPreparer implements PreparerBase { ); switch (type) { case 'url': { - const response = await this.reader.readTree(target); - return await response.dir(); + const response = await this.reader.readTree(target, { + etag: options?.etag, + }); + const preparedDir = await response.dir(); + return { + preparedDir, + etag: response.etag, + }; } case 'github': case 'gitlab': @@ -57,29 +74,47 @@ export class DirectoryPreparer implements PreparerBase { this.logger, ); - return path.dirname( - path.join(repoLocation, parsedGitLocation.filepath), - ); + // Check if etag has changed for cache invalidation. + const etag = await getLastCommitTimestamp(repoLocation, this.logger); + if (options?.etag === etag.toString()) { + throw new NotModifiedError(); + } + return { + preparedDir: path.dirname( + path.join(repoLocation, parsedGitLocation.filepath), + ), + etag: etag.toString(), + }; } case 'file': - return path.dirname(target); + return { + preparedDir: path.dirname(target), + // Instead of supporting caching on local sources, use techdocs-cli for local development and debugging. + etag: '', + }; default: throw new InputError(`Unable to resolve location type ${type}`); } } - async prepare(entity: Entity): Promise { + async prepare(entity: Entity): Promise { + this.logger.warn( + 'You are using the legacy dir preparer in TechDocs which will be removed in near future (March 2021). ' + + 'Migrate to URL reader by updating `backstage.io/techdocs-ref` annotation in `catalog-info.yaml` ' + + 'to be prefixed with `url:`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 ', + ); + const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); - const managedByLocationDirectory = await this.resolveManagedByLocationToDir( - entity, - ); + // This will throw NotModified error if etag has not changed. + const response = await this.resolveManagedByLocationToDir(entity); - return new Promise(resolve => { - resolve(path.resolve(managedByLocationDirectory, target)); - }); + return { + preparedDir: path.resolve(response.preparedDir, target), + etag: response.etag, + }; } } diff --git a/packages/techdocs-common/src/stages/prepare/types.ts b/packages/techdocs-common/src/stages/prepare/types.ts index 97e75306c3..96510e8eec 100644 --- a/packages/techdocs-common/src/stages/prepare/types.ts +++ b/packages/techdocs-common/src/stages/prepare/types.ts @@ -16,13 +16,31 @@ import type { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; +export type PreparerResponse = { + /** + * The path to directory where the tree is downloaded. + */ + preparedDir: string; + /** + * A unique identifer of the tree blob, usually the commit SHA or etag from the target. + */ + etag: string; +}; + export type PreparerBase = { /** * Given an Entity definition from the Service Catalog, go and prepare a directory - * with contents from the location in temporary storage and return the path + * with contents from the location in temporary storage and return the path. + * * @param entity The entity from the Service Catalog + * @param options.etag (Optional) If etag is provider, it will be used to check if the target has + * updated since the last build. + * @throws {NotModifiedError} when the prepared directory has not been changed since the last build. */ - prepare(entity: Entity, opts?: { logger: Logger }): Promise; + prepare( + entity: Entity, + options?: { logger?: Logger; etag?: string }, + ): Promise; }; export type PreparerBuilder = { @@ -30,10 +48,14 @@ export type PreparerBuilder = { get(entity: Entity): PreparerBase; }; +/** + * Everything except `url` will be deprecated. + * Read more https://github.com/backstage/backstage/issues/4409 + */ export type RemoteProtocol = + | 'url' | 'dir' | 'github' | 'gitlab' | 'file' - | 'azure/api' - | 'url'; + | 'azure/api'; diff --git a/packages/techdocs-common/src/stages/prepare/url.ts b/packages/techdocs-common/src/stages/prepare/url.ts index b938c6b5bc..9e4715a4ee 100644 --- a/packages/techdocs-common/src/stages/prepare/url.ts +++ b/packages/techdocs-common/src/stages/prepare/url.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Logger } from 'winston'; +import { NotModifiedError, UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { UrlReader } from '@backstage/backend-common'; -import { PreparerBase } from './types'; +import { Logger } from 'winston'; import { getDocFilesFromRepository } from '../../helpers'; +import { PreparerBase, PreparerResponse } from './types'; export class UrlPreparer implements PreparerBase { private readonly logger: Logger; @@ -28,13 +28,25 @@ export class UrlPreparer implements PreparerBase { this.reader = reader; } - async prepare(entity: Entity): Promise { + async prepare( + entity: Entity, + options?: { etag?: string }, + ): Promise { try { - return getDocFilesFromRepository(this.reader, entity); + return await getDocFilesFromRepository(this.reader, entity, { + etag: options?.etag, + logger: this.logger, + }); } catch (error) { - this.logger.debug( - `Unable to fetch files for building docs ${error.message}`, - ); + // NotModifiedError means that etag based cache is still valid. + if (error instanceof NotModifiedError) { + this.logger.debug(`Cache is valid for etag ${options?.etag}`); + } else { + this.logger.debug( + `Unable to fetch files for building docs ${error.message}`, + ); + } + throw error; } } diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts old mode 100755 new mode 100644 index 3902aab21a..813f6b8554 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -102,6 +102,12 @@ describe('AwsS3Publish', () => { }); it('should fail to publish a directory', async () => { + const wrongPathToGeneratedDirectory = path.join( + 'wrong', + 'path', + 'to', + 'generatedDirectory', + ); const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); @@ -118,11 +124,13 @@ describe('AwsS3Publish', () => { await publisher .publish({ entity, - directory: '/wrong/path/to/generatedDirectory', + directory: wrongPathToGeneratedDirectory, }) .catch(error => - expect(error.message).toContain( - 'Unable to upload file(s) to AWS S3. Error Failed to read template directory', + expect(error).toEqual( + new Error( + `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + ), ), ); mockFs.restore(); @@ -198,17 +206,14 @@ describe('AwsS3Publish', () => { it('should return an error if the techdocs_metadata.json file is not present', async () => { const entityNameMock = createMockEntityName(); const entity = createMockEntity(); - const { - metadata: { name, namespace }, - kind, - } = entity; + const entityRootDir = getEntityRootDir(entity); await publisher .fetchTechDocsMetadata(entityNameMock) .catch(error => expect(error).toEqual( new Error( - `TechDocs metadata fetch failed, The file ${namespace}/${kind}/${name}/techdocs_metadata.json doest not exist.`, + `TechDocs metadata fetch failed, The file ${entityRootDir}/techdocs_metadata.json doest not exist !`, ), ), ); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 20e6bc424d..8eaf03eb2f 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -15,7 +15,8 @@ */ import path from 'path'; import express from 'express'; -import { PutObjectCommandOutput, S3 } from '@aws-sdk/client-s3'; +import aws from 'aws-sdk'; +import { ManagedUpload } from 'aws-sdk/clients/s3'; import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -71,7 +72,7 @@ export class AwsS3Publish implements PublisherBase { // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html const region = config.getOptionalString('techdocs.publisher.awsS3.region'); - const storageClient = new S3({ + const storageClient = new aws.S3({ ...(credentials && accessKeyId && secretAccessKey && { @@ -113,7 +114,7 @@ export class AwsS3Publish implements PublisherBase { } constructor( - private readonly storageClient: S3, + private readonly storageClient: aws.S3, private readonly bucketName: string, private readonly logger: Logger, ) { @@ -133,7 +134,7 @@ export class AwsS3Publish implements PublisherBase { const allFilesToUpload = await getFileTreeRecursively(directory); const limiter = createLimiter(10); - const uploadPromises: Array> = []; + const uploadPromises: Array> = []; for (const filePath of allFilesToUpload) { // Remove the absolute path prefix of the source directory // Path of all files to upload, relative to the root of the source directory @@ -151,7 +152,9 @@ export class AwsS3Publish implements PublisherBase { }; // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter(() => this.storageClient.putObject(params)); + const uploadFile = limiter(() => + this.storageClient.upload(params).promise(), + ); uploadPromises.push(uploadFile); } await Promise.all(uploadPromises); @@ -170,34 +173,33 @@ export class AwsS3Publish implements PublisherBase { entityName: EntityName, ): Promise { try { - return await new Promise((resolve, reject) => { + return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; - this.storageClient + const stream = this.storageClient .getObject({ Bucket: this.bucketName, Key: `${entityRootDir}/techdocs_metadata.json`, }) - .then(async file => { - const techdocsMetadataJson = await streamToBuffer( - file.Body as Readable, - ); + .createReadStream(); - if (!techdocsMetadataJson) { - throw new Error( - `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, - ); - } - const techdocsMetadata = JSON5.parse( - techdocsMetadataJson.toString('utf-8'), + try { + const techdocsMetadataJson = await streamToBuffer(stream); + if (!techdocsMetadataJson) { + throw new Error( + `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, ); + } - resolve(techdocsMetadata); - }) - .catch(err => { - this.logger.error(err.message); - reject(new Error(err.message)); - }); + const techdocsMetadata = JSON5.parse( + techdocsMetadataJson.toString('utf-8'), + ); + + resolve(techdocsMetadata); + } catch (err) { + this.logger.error(err.message); + reject(new Error(err.message)); + } }); } catch (e) { throw new Error(`TechDocs metadata fetch failed, ${e.message}`); @@ -208,7 +210,7 @@ export class AwsS3Publish implements PublisherBase { * Express route middleware to serve static files on a route in techdocs-backend. */ docsRouter(): express.Handler { - return (req, res) => { + return async (req, res) => { // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html const filePath = req.path.replace(/^\//, ''); @@ -217,27 +219,22 @@ export class AwsS3Publish implements PublisherBase { const fileExtension = path.extname(filePath); const responseHeaders = getHeadersForFileExtension(fileExtension); - this.storageClient + const stream = this.storageClient .getObject({ Bucket: this.bucketName, Key: filePath }) - .then(async object => { - const fileContent = await streamToBuffer(object.Body as Readable); - if (!fileContent) { - throw new Error(`Unable to parse the file ${filePath}.`); - } + .createReadStream(); + try { + // Inject response headers + for (const [headerKey, headerValue] of Object.entries( + responseHeaders, + )) { + res.setHeader(headerKey, headerValue); + } - // Inject response headers - for (const [headerKey, headerValue] of Object.entries( - responseHeaders, - )) { - res.setHeader(headerKey, headerValue); - } - - res.send(fileContent); - }) - .catch(err => { - this.logger.warn(err.message); - res.status(404).send(err.message); - }); + res.send(await streamToBuffer(stream)); + } catch (err) { + this.logger.warn(err.message); + res.status(404).send(err.message); + } }; } @@ -248,10 +245,12 @@ export class AwsS3Publish implements PublisherBase { async hasDocsBeenGenerated(entity: Entity): Promise { try { const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - await this.storageClient.headObject({ - Bucket: this.bucketName, - Key: `${entityRootDir}/index.html`, - }); + await this.storageClient + .headObject({ + Bucket: this.bucketName, + Key: `${entityRootDir}/index.html`, + }) + .promise(); return Promise.resolve(true); } catch (e) { return Promise.resolve(false); diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index e349a2119a..ac09331dcd 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -88,7 +88,7 @@ export class LocalPublish implements PublisherBase { ); reject(err); } - + this.logger.info(`Published site stored at ${publishDir}`); this.discovery .getBaseUrl('techdocs') .then(techdocsApiUrl => { diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 6d23f26691..a30b2b3f6a 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/test-utils +## 0.1.7 + +### Patch Changes + +- b51ee6ece: Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components. + ## 0.1.6 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5cea83fa03..5a4ad2b4a4 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.6", + "version": "0.1.7", "private": false, "publishConfig": { "access": "public", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 4735beb150..47379a9b18 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-api-docs +## 0.4.5 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.4.4 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 175b831ada..8df169c1d9 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.4.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "dependencies": { "@asyncapi/react-component": "^0.18.2", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -50,8 +50,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index b57e0d25fb..f39dad2eb9 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-app-backend +## 0.3.7 + +### Patch Changes + +- 727f0deec: Added a new `disableConfigInjection` option, which can be used to disable the configuration injection in environments where it can't be used. +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + ## 0.3.6 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index cca36a51f8..52ebab520d 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/config-loader": "^0.5.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index e3ff6aaa43..e97d79c4ae 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-auth-backend +## 0.3.0 + +### Minor Changes + +- 1deb31141: Remove undocumented scope (default) from the OIDC auth provider which was breaking some identity services. If your app relied on this scope, you can manually specify it by adding a new factory in `packages/app/src/apis.ts`: + + ``` + export const apis = [ + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: OAuth2Icon, + }, + defaultScopes: [ + 'default', + 'openid', + 'email', + 'offline_access', + ], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + ]; + ``` + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- 07bafa248: Add configurable `scope` for oauth2 auth provider. + + Some OAuth2 providers require certain scopes to facilitate a user sign-in using the Authorization Code flow. + This change adds the optional `scope` key to auth.providers.oauth2. An example is: + + ```yaml + auth: + providers: + oauth2: + development: + clientId: + $env: DEV_OAUTH2_CLIENT_ID + clientSecret: + $env: DEV_OAUTH2_CLIENT_SECRET + authorizationUrl: + $env: DEV_OAUTH2_AUTH_URL + tokenUrl: + $env: DEV_OAUTH2_TOKEN_URL + scope: saml-login-selector openid profile email + ``` + + This tells the OAuth 2.0 AS to perform a SAML login and return OIDC information include the `profile` + and `email` claims as part of the ID Token. + +- Updated dependencies [6ed2b47d6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [72b96e880] +- Updated dependencies [5a5163519] + - @backstage/catalog-client@0.3.6 + - @backstage/backend-common@0.5.3 + ## 0.2.12 ### Patch Changes diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 84f71b52b9..9850e05235 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -58,7 +58,13 @@ export interface Config { development: { [key: string]: string }; }; oauth2?: { - development: { [key: string]: string }; + development: { + clientId: string; + clientSecret: string; + authorizationUrl: string; + tokenUrl: string; + scope?: string; + }; }; oidc?: { development: { [key: string]: string }; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index e87c2b4a90..c50789d4c1 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.2.12", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", - "@backstage/catalog-client": "^0.3.5", + "@backstage/backend-common": "^0.5.3", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "jose": "^1.27.1", "jwt-decode": "^3.1.0", "knex": "^0.21.6", - "moment": "^2.26.0", + "luxon": "^1.25.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", "openid-client": "^4.2.1", diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts index b22ab0ecda..8896a0d158 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts @@ -15,8 +15,8 @@ */ import Knex from 'knex'; -import moment from 'moment'; import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { DateTime } from 'luxon'; function createDB() { const knex = Knex({ @@ -51,7 +51,11 @@ describe('DatabaseKeyStore', () => { const { items } = await store.listKeys(); expect(items).toEqual([{ createdAt: expect.anything(), key }]); - expect(Math.abs(items[0].createdAt.diff(moment(), 's'))).toBeLessThan(10); + expect( + Math.abs( + DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds, + ), + ).toBeLessThan(10); }); it('should remove stored keys', async () => { diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index c24cac55be..e53102e333 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -15,9 +15,9 @@ */ import Knex from 'knex'; -import { utc } from 'moment'; import { resolvePackagePath } from '@backstage/backend-common'; import { AnyJWK, KeyStore, StoredKey } from './types'; +import { DateTime } from 'luxon'; const migrationsDir = resolvePackagePath( '@backstage/plugin-auth-backend', @@ -27,7 +27,7 @@ const migrationsDir = resolvePackagePath( const TABLE = 'signing_keys'; type Row = { - created_at: Date; + created_at: Date; // row.created_at is a string after being returned from the database kid: string; key: string; }; @@ -36,6 +36,21 @@ type Options = { database: Knex; }; +const parseDate = (date: string | Date) => { + const parsedDate = + typeof date === 'string' + ? DateTime.fromSQL(date, { locale: 'UTC' }) + : DateTime.fromJSDate(date); + + if (!parsedDate.isValid) { + throw new Error( + `Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`, + ); + } + + return parsedDate.toJSDate(); +}; + export class DatabaseKeyStore implements KeyStore { static async create(options: Options): Promise { const { database } = options; @@ -66,7 +81,7 @@ export class DatabaseKeyStore implements KeyStore { return { items: rows.map(row => ({ key: JSON.parse(row.key), - createdAt: utc(row.created_at), + createdAt: parseDate(row.created_at), })), }; } diff --git a/plugins/auth-backend/src/identity/MemoryKeyStore.ts b/plugins/auth-backend/src/identity/MemoryKeyStore.ts index b35002b146..3dc1d777c9 100644 --- a/plugins/auth-backend/src/identity/MemoryKeyStore.ts +++ b/plugins/auth-backend/src/identity/MemoryKeyStore.ts @@ -14,18 +14,15 @@ * limitations under the License. */ -import { utc } from 'moment'; import { KeyStore, AnyJWK, StoredKey } from './types'; +import { DateTime } from 'luxon'; export class MemoryKeyStore implements KeyStore { - private readonly keys = new Map< - string, - { createdAt: moment.Moment; key: string } - >(); + private readonly keys = new Map(); async addKey(key: AnyJWK): Promise { this.keys.set(key.kid, { - createdAt: utc(), + createdAt: DateTime.utc().toJSDate(), key: JSON.stringify(key), }); } diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 36622332e0..1c6192d5c5 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import moment from 'moment'; import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types'; import { JSONWebKey, JWK, JWS } from 'jose'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; +import { DateTime } from 'luxon'; const MS_IN_S = 1000; @@ -52,7 +52,7 @@ export class TokenFactory implements TokenIssuer { private readonly keyStore: KeyStore; private readonly keyDurationSeconds: number; - private keyExpiry?: moment.Moment; + private keyExpiry?: Date; private privateKeyPromise?: Promise; constructor(options: Options) { @@ -90,8 +90,10 @@ export class TokenFactory implements TokenIssuer { for (const key of keys) { // Allow for a grace period of another full key duration before we remove the keys from the database - const expireAt = key.createdAt.add(3 * this.keyDurationSeconds, 's'); - if (expireAt.isBefore()) { + const expireAt = DateTime.fromJSDate(key.createdAt).plus({ + seconds: 3 * this.keyDurationSeconds, + }); + if (expireAt < DateTime.local()) { expiredKeys.push(key); } else { validKeys.push(key); @@ -117,14 +119,21 @@ export class TokenFactory implements TokenIssuer { private async getKey(): Promise { // Make sure that we only generate one key at a time if (this.privateKeyPromise) { - if (this.keyExpiry?.isAfter()) { + if ( + this.keyExpiry && + DateTime.fromJSDate(this.keyExpiry) > DateTime.local() + ) { return this.privateKeyPromise; } this.logger.info(`Signing key has expired, generating new key`); delete this.privateKeyPromise; } - this.keyExpiry = moment().add(this.keyDurationSeconds, 'seconds'); + this.keyExpiry = DateTime.utc() + .plus({ + seconds: this.keyDurationSeconds, + }) + .toJSDate(); const promise = (async () => { // This generates a new signing key to be used to sign tokens until the next key rotation const key = await JWK.generate('EC', 'P-256', { diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index 827aba51ab..6e984d41cc 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -52,7 +52,7 @@ export type TokenIssuer = { */ export type StoredKey = { key: AnyJWK; - createdAt: moment.Moment; + createdAt: Date; }; /** diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index ec6aa1b6ba..4d4bcaf2bf 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -38,11 +38,14 @@ describe('CatalogIdentityClient', () => { client.findUser({ annotations: { key: 'value' } }); - expect(catalogApi.getEntities).toBeCalledWith({ - filter: { - kind: 'user', - 'metadata.annotations.key': 'value', + expect(catalogApi.getEntities).toBeCalledWith( + { + filter: { + kind: 'user', + 'metadata.annotations.key': 'value', + }, }, - }); + undefined, + ); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 5bc4e5de21..e1a3553394 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -37,7 +37,10 @@ export class CatalogIdentityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple users are found. */ - async findUser(query: UserQuery): Promise { + async findUser( + query: UserQuery, + options?: { token?: string }, + ): Promise { const filter: Record = { kind: 'user', }; @@ -45,7 +48,7 @@ export class CatalogIdentityClient { filter[`metadata.annotations.${key}`] = value; } - const { items } = await this.catalogApi.getEntities({ filter }); + const { items } = await this.catalogApi.getEntities({ filter }, options); if (items.length !== 1) { if (items.length > 1) { diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index ae4f5fabd0..848a02bad6 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -38,6 +38,7 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { AuthProviderFactory, RedirectInfo } from '../types'; +import { TokenIssuer } from '../../identity'; type PrivateInfo = { refreshToken: string; @@ -46,16 +47,19 @@ type PrivateInfo = { export type GoogleAuthProviderOptions = OAuthProviderOptions & { logger: Logger; identityClient: CatalogIdentityClient; + tokenIssuer: TokenIssuer; }; export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; private readonly logger: Logger; private readonly identityClient: CatalogIdentityClient; + private readonly tokenIssuer: TokenIssuer; constructor(options: GoogleAuthProviderOptions) { this.logger = options.logger; this.identityClient = options.identityClient; + this.tokenIssuer = options.tokenIssuer; // TODO: throw error if env variables not set? this._strategy = new GoogleStrategy( { @@ -150,11 +154,17 @@ export class GoogleAuthProvider implements OAuthHandlers { } try { - const user = await this.identityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, + const token = await this.tokenIssuer.issueToken({ + claims: { sub: 'backstage.io/auth-backend' }, }); + const user = await this.identityClient.findUser( + { + annotations: { + 'google.com/email': profile.email, + }, + }, + { token }, + ); return { ...response, @@ -192,6 +202,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ clientSecret, callbackUrl, logger, + tokenIssuer, identityClient: new CatalogIdentityClient({ catalogApi }), }); diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 04cb7670f3..a44c4fb50b 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -44,6 +44,7 @@ type PrivateInfo = { export type OAuth2AuthProviderOptions = OAuthProviderOptions & { authorizationUrl: string; tokenUrl: string; + scope?: string; }; export class OAuth2AuthProvider implements OAuthHandlers { @@ -58,6 +59,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { authorizationURL: options.authorizationUrl, tokenURL: options.tokenUrl, passReqToCallback: false as true, + scope: options.scope, }, ( accessToken: any, @@ -168,6 +170,7 @@ export const createOAuth2Provider: AuthProviderFactory = ({ const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; const authorizationUrl = envConfig.getString('authorizationUrl'); const tokenUrl = envConfig.getString('tokenUrl'); + const scope = envConfig.getOptionalString('scope'); const provider = new OAuth2AuthProvider({ clientId, @@ -175,6 +178,7 @@ export const createOAuth2Provider: AuthProviderFactory = ({ callbackUrl, authorizationUrl, tokenUrl, + scope, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9e44a82c37..6ea7b8a8d3 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -65,7 +65,7 @@ export class OidcAuthProvider implements OAuthHandlers { return await executeRedirectStrategy(req, strategy, { accessType: 'offline', prompt: 'none', - scope: `${req.scope} default`, + scope: req.scope, state: encodeState(req.state), }); } diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index eb256d2c08..9ae261ecf3 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend +## 0.6.1 + +### Patch Changes + +- 77ad0003a: Revert AWS SDK version to v2 +- d2441aee3: use child logger, if provided, to log single location refresh +- fb53eb7cb: Don't respond to a request twice if an entity has not been found. +- f3fbfb452: add indices on columns referring locations(id) +- 84364b35c: Added an option to scan GitHub for repositories using a new location type `github-discovery`. + Example: + + ```yaml + type: 'github-discovery', + target: + 'https://github.com/backstage/techdocs-*/blob/master/catalog.yaml' + ``` + + You can use wildcards (`*`) as well. This will add `location` entities for each matching repository. + Currently though, you must specify the exact path of the `catalog.yaml` file in the repository. + +- 82b2c11b6: Refactored route response handling to use more explicit types and throw errors. +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + ## 0.6.0 ### Minor Changes diff --git a/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js new file mode 100644 index 0000000000..5591717c13 --- /dev/null +++ b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js @@ -0,0 +1,45 @@ +/* + * 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) { + if (knex.client.config.client !== 'sqlite3') { + await knex.schema.alterTable('entities', table => { + table.index('location_id', 'entity_location_id_idx'); + }); + await knex.schema.alterTable('location_update_log', table => { + table.index('location_id', 'update_log_location_id_idx'); + }); + } +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + if (knex.client.config.client !== 'sqlite3') { + await knex.schema.alterTable('entities', table => { + table.dropIndex([], 'entity_location_id_idx'); + }); + await knex.schema.alterTable('location_update_log', table => { + table.dropIndex([], 'update_log_location_id_idx'); + }); + } +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index bb76441f3f..254898a086 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.6.0", + "version": "0.6.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,15 +29,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@aws-sdk/client-organizations": "^3.2.0", "@azure/msal-node": "^1.0.0-beta.3", - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", + "aws-sdk": "^2.840.0", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "cross-fetch": "^3.0.6", @@ -59,7 +59,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index 28200021cc..ba163fa0c3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -27,18 +27,22 @@ describe('AwsOrganizationCloudAccountProcessor', () => { afterEach(() => jest.resetAllMocks()); it('generates component entities for accounts', async () => { - listAccounts.mockImplementation(() => - Promise.resolve({ - Accounts: [ - { - Arn: - 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - ], - NextToken: undefined, - }), - ); + listAccounts.mockImplementation(() => { + return { + async promise() { + return { + Accounts: [ + { + Arn: + 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', + }, + ], + NextToken: undefined, + }; + }, + }; + }); await processor.readLocation(location, false, emit); expect(emit).toBeCalledWith({ type: 'entity', @@ -69,23 +73,27 @@ describe('AwsOrganizationCloudAccountProcessor', () => { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3', }; - listAccounts.mockImplementation(() => - Promise.resolve({ - Accounts: [ - { - Arn: - 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - { - Arn: - 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', - Name: 'testaccount2', - }, - ], - NextToken: undefined, - }), - ); + listAccounts.mockImplementation(() => { + return { + async promise() { + return { + Accounts: [ + { + Arn: + 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', + }, + { + Arn: + 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', + Name: 'testaccount2', + }, + ], + NextToken: undefined, + }; + }, + }; + }); await processor.readLocation(locationTest, false, emit); expect(emit).toBeCalledTimes(1); expect(emit).toBeCalledWith({ diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts index 40516f88f7..0e988e7307 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts @@ -14,11 +14,8 @@ * limitations under the License. */ import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model'; -import { - Account, - Organizations, - ListAccountsCommandOutput, -} from '@aws-sdk/client-organizations'; +import AWS, { Organizations } from 'aws-sdk'; +import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; @@ -38,7 +35,7 @@ const ORGANIZATION_ANNOTATION: string = 'amazonaws.com/organization-id'; export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { organizations: Organizations; constructor() { - this.organizations = new Organizations({ + this.organizations = new AWS.Organizations({ region: AWS_ORGANIZATION_REGION, }); // Only available in us-east-1 } @@ -67,9 +64,9 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { let nextToken = undefined; while (isInitialAttempt || nextToken) { isInitialAttempt = false; - const orgAccounts: ListAccountsCommandOutput = await this.organizations.listAccounts( - { NextToken: nextToken }, - ); + const orgAccounts: ListAccountsResponse = await this.organizations + .listAccounts({ NextToken: nextToken }) + .promise(); if (orgAccounts.Accounts) { awsAccounts = awsAccounts.concat(orgAccounts.Accounts); } diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts new file mode 100644 index 0000000000..41698e1d04 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -0,0 +1,222 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; +import { getOrganizationRepositories } from './github'; +import { ConfigReader } from '@backstage/config'; + +jest.mock('./github'); +const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< + typeof getOrganizationRepositories +>; + +describe('GithubDiscoveryProcessor', () => { + describe('parseUrl', () => { + it('parses well formed URLs', () => { + expect( + parseUrl('https://github.com/foo/proj/blob/master/catalog.yaml'), + ).toEqual({ + org: 'foo', + repoSearchPath: /^proj$/, + catalogPath: '/blob/master/catalog.yaml', + }); + expect( + parseUrl('https://github.com/foo/proj*/blob/master/catalog.yaml'), + ).toEqual({ + org: 'foo', + repoSearchPath: /^proj.*$/, + catalogPath: '/blob/master/catalog.yaml', + }); + }); + + it('throws on incorrectly formed URLs', () => { + expect(() => parseUrl('https://github.com')).toThrow(); + expect(() => parseUrl('https://github.com//')).toThrow(); + expect(() => parseUrl('https://github.com/foo')).toThrow(); + expect(() => parseUrl('https://github.com//foo')).toThrow(); + expect(() => parseUrl('https://github.com/org/teams')).toThrow(); + expect(() => parseUrl('https://github.com/org//teams')).toThrow(); + }); + }); + + describe('reject unrelated entries', () => { + it('rejects unknown types', async () => { + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'not-github-discovery', + target: 'https://github.com', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).resolves.toBeFalsy(); + }); + + it('rejects unknown targets', async () => { + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'blob' }, + { host: 'ghe.example.net', token: 'blob' }, + ], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://not.github.com/apa', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no GitHub integration that matches https:\/\/not.github.com\/apa/, + ); + }); + }); + + describe('handles repositories', () => { + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + + beforeEach(() => { + mockGetOrganizationRepositories.mockClear(); + }); + + it('output all repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://github.com/backstage/*/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { name: 'demo', url: 'https://github.com/backstage/demo' }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog.yaml', + }, + optional: false, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: 'https://github.com/backstage/demo/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + + it('output repositories with wildcards', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: + 'https://github.com/backstage/techdocs-*/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'techdocs-cli', + url: 'https://github.com/backstage/techdocs-cli', + }, + { + name: 'techdocs-container', + url: 'https://github.com/backstage/techdocs-container', + }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/techdocs-cli/blob/master/catalog.yaml', + }, + optional: false, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/techdocs-container/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + it('filter unrelated repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'abstest', url: 'https://github.com/backstage/abctest' }, + { + name: 'test', + url: 'https://github.com/backstage/test', + }, + { + name: 'testxyz', + url: 'https://github.com/backstage/testxyz', + }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts new file mode 100644 index 0000000000..9af247fd25 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2021 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 { Config } from '@backstage/config'; +import { + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { graphql } from '@octokit/graphql'; +import { Logger } from 'winston'; +import { getOrganizationRepositories } from './github'; +import * as results from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; + +/** + * Extracts repositories out of a GitHub org. + */ +export class GithubDiscoveryProcessor implements CatalogProcessor { + private readonly integrations: ScmIntegrations; + private readonly logger: Logger; + + static fromConfig(config: Config, options: { logger: Logger }) { + const integrations = ScmIntegrations.fromConfig(config); + + return new GithubDiscoveryProcessor({ + ...options, + integrations, + }); + } + + constructor(options: { integrations: ScmIntegrations; logger: Logger }) { + this.integrations = options.integrations; + this.logger = options.logger; + } + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise { + if (location.type !== 'github-discovery') { + return false; + } + + const gitHubConfig = this.integrations.github.byUrl(location.target) + ?.config; + if (!gitHubConfig) { + throw new Error( + `There is no GitHub integration that matches ${location.target}. Please add a configuration entry for it under integrations.github`, + ); + } + const { headers } = await GithubCredentialsProvider.create( + gitHubConfig, + ).getCredentials({ url: location.target }); + const { org, repoSearchPath, catalogPath } = parseUrl(location.target); + + const client = graphql.defaults({ + baseUrl: gitHubConfig.apiBaseUrl, + headers, + }); + + // Read out all of the raw data + const startTimestamp = Date.now(); + this.logger.info(`Reading GitHub repositories from ${location.target}`); + + const { repositories } = await getOrganizationRepositories(client, org); + + const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); + this.logger.debug( + `Read ${repositories.length} GitHub repositories in ${duration} seconds`, + ); + + for (const repository of repositories) { + if (!repoSearchPath.test(repository.name)) { + continue; + } + emit( + results.location( + { + type: 'url', + target: `${repository.url}${catalogPath}`, + }, + false, + ), + ); + } + + return true; + } +} + +/* + * Helpers + */ + +export function parseUrl( + urlString: string, +): { org: string; repoSearchPath: RegExp; catalogPath: string } { + const url = new URL(urlString); + const path = url.pathname.substr(1).split('/'); + + // /backstage/techdocs-*/blob/master/catalog-info.yaml + if (path.length > 2 && path[0].length && path[1].length) { + return { + org: decodeURIComponent(path[0]), + repoSearchPath: escapeRegExp(decodeURIComponent(path[1])), + catalogPath: `/${decodeURIComponent(path.slice(2).join('/'))}`, + }; + } + + throw new Error(`Failed to parse ${urlString}`); +} + +export function escapeRegExp(str: string): RegExp { + return new RegExp(`^${str.replace(/\*/g, '.*')}$`); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts index 8911e84d99..81b44c706d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts @@ -22,6 +22,7 @@ import { getOrganizationTeams, getOrganizationUsers, getTeamMembers, + getOrganizationRepositories, QueryResponse, } from './github'; @@ -151,4 +152,48 @@ describe('github', () => { await expect(getTeamMembers(graphql, 'a', 'b')).resolves.toEqual(output); }); }); + + describe('getOrganizationRepositories', () => { + it('read repositories', async () => { + const input: QueryResponse = { + organization: { + repositories: { + nodes: [ + { + name: 'backstage', + url: 'https://github.com/backstage/backstage', + }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + }; + + const output = { + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + }, + ], + }; + + server.use( + graphqlMsw.query('repositories', (_req, res, ctx) => + res(ctx.data(input)), + ), + ); + + await expect(getOrganizationRepositories(graphql, 'a')).resolves.toEqual( + output, + ); + }); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.ts index 2b33d72f65..d50887c592 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.ts @@ -27,6 +27,7 @@ export type Organization = { membersWithRole?: Connection; team?: Team; teams?: Connection; + repositories?: Connection; }; export type PageInfo = { @@ -52,6 +53,11 @@ export type Team = { members: Connection; }; +export type Repository = { + name: string; + url: string; +}; + export type Connection = { pageInfo: PageInfo; nodes: T[]; @@ -216,6 +222,39 @@ export async function getOrganizationTeams( return { groups, groupMemberUsers }; } +export async function getOrganizationRepositories( + client: typeof graphql, + org: string, +): Promise<{ repositories: Repository[] }> { + const query = ` + query repositories($org: String!, $cursor: String) { + organization(login: $org) { + name + repositories(first: 100, after: $cursor) { + nodes { + name + url + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `; + + const repositories = await queryWithPaging( + client, + query, + r => r.organization?.repositories, + x => x, + { org }, + ); + + return { repositories }; +} + /** * Gets all the users out of a GitHub organization. * diff --git a/plugins/catalog-backend/src/ingestion/processors/github/index.ts b/plugins/catalog-backend/src/ingestion/processors/github/index.ts index e424d7bafb..2063e8c1b2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/index.ts @@ -16,4 +16,8 @@ export { readGithubConfig } from './config'; export type { ProviderConfig } from './config'; -export { getOrganizationTeams, getOrganizationUsers } from './github'; +export { + getOrganizationTeams, + getOrganizationUsers, + getOrganizationRepositories, +} from './github'; diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 05dd327f0e..736c64ea39 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -21,6 +21,7 @@ export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAcco export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; +export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index f7544ef42e..a75c7da6e8 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -43,6 +43,7 @@ import { CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, + GithubDiscoveryProcessor, GithubOrgReaderProcessor, HigherOrderOperation, HigherOrderOperations, @@ -303,6 +304,7 @@ export class CatalogBuilder { if (!this.processorsReplace) { processors.push( new FileReaderProcessor(), + GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 0311d865a9..3966557555 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-catalog-import +## 0.4.0 + +### Minor Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- 68dd79d83: The plugin has been refactored and is now based on a configurable state machine of 'analyze', 'prepare', 'review' & 'finish'. + Depending on the outcome of the 'analyze' stage, different flows are selected ('single-location', 'multiple-locations', 'no-location'). + Each flow can define it's own components that guide the user. + + During the refactoring, the `catalogRouteRef` property of the `CatalogImportPage` has been removed, so the `App.tsx` of the backstage apps need to be updated: + + ```diff + // packages/app/src/App.tsx + + } + + element={} + /> + ``` + +### Patch Changes + +- 753bb4c40: Flatten the options of the `CatalogImportPage` from the `options` property to the `pullRequest` property. + + ```diff + - + + + ``` + +- Updated dependencies [6ed2b47d6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [72b96e880] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/catalog-client@0.3.6 + - @backstage/integration@0.4.0 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.7 ### Patch Changes diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index 1dc0d7cf89..97371ce0e7 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -1,21 +1,84 @@ -# Catalog import plugin +# Catalog Import -Welcome to the catalog-import plugin! +The Catalog Import Plugin provides a wizard to onboard projects with existing `catalog-info.yaml` files. +It also assists by creating pull requests in repositories where no `catalog-info.yaml` exists. -This plugin allows you to bootstrap a component-config YAML file for your repository and open a pull request to add it. + -When installed it is accessible on [localhost:3000/catalog-import](localhost:3000/catalog-import). +Current features: - +- Import `catalog-info.yaml` files from a URL in a repository of one of the supported Git integrations (example `https://github.com/backstage/backstage/catalog-info.yaml`). +- _[GitHub only]_ Search for all `catalog-info.yaml` files in a Git repository (example: `https://github.com/backstage/backstage`). +- _[GitHub only]_ Analyze a repository, generate a Component entity, and create a Pull Request to onboard the repository. -## Running +Some features are not yet available for all supported Git providers. -Just run the backstage. +## Getting Started -``` -yarn start && yarn --cwd packages/backend start +1. Install the Catalog Import Plugin: + +```bash +# packages/app + +yarn add @backstage/plugin-catalog-import ``` -## Usage +2. Add the plugin to the app: -Pretty straightforward, navigate to [localhost:3000/catalog-import](localhost:3000/catalog-import) and enter your repo's URL. +```ts +// packages/app/src/plugins.ts + +export { catalogImportPlugin } from '@backstage/plugin-catalog-import'; +``` + +3. Register the `CatalogImportPage` at the `/catalog-import` path: + +```tsx +// packages/app/src/App.tsx + +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; + +} />; +``` + +## Customizations + +### Disable the creation of Pull Requests + +The pull request feature can be disabled by options that are passed to the `CatalogImportPage`: + +```tsx +// packages/app/src/App.tsx + +} +/> +``` + +### Customize the title and body of the Pull Request + +The pull request form is filled with a default title and body. +This can be configured by options that are passed to the `CatalogImportPage`: + +```tsx +// packages/app/src/App.tsx + + ({ + title: 'My title', + body: 'My **markdown** body', + }), + }} + /> + } +/> +``` + +## Development + +Use `yarn start` to run a [development version](./dev/index.tsx) of the plugin that can be used to validate each flow with mocked data. diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx index c9b46a7ed4..e794628e6c 100644 --- a/plugins/catalog-import/dev/index.tsx +++ b/plugins/catalog-import/dev/index.tsx @@ -14,7 +14,338 @@ * limitations under the License. */ +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity, EntityName } from '@backstage/catalog-model'; +import { Content, Header, InfoCard, Page } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { catalogImportPlugin } from '../src/plugin'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; +import AlarmIcon from '@material-ui/icons/Alarm'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React from 'react'; +import { + AnalyzeResult, + CatalogImportApi, + catalogImportApiRef, + EntityListComponent, + ImportStepper, +} from '../src'; +import { ImportComponentPage } from '../src/components/ImportComponentPage'; -createDevApp().registerPlugin(catalogImportPlugin).render(); +const getEntityNames = (url: string): EntityName[] => [ + { + kind: 'Component', + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'component-a', + }, + { + kind: 'API', + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'api-a', + }, +]; + +const getEntities = (url: string): Entity[] => [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'component-a', + }, + }, + { + apiVersion: '1', + kind: 'API', + metadata: { + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'api-a', + }, + }, +]; + +const locations = [ + { + target: 'https://my-location-1', + entities: [ + { + kind: 'Domain', + namespace: 'default', + name: 'my-domain', + }, + { + kind: 'Group', + namespace: 'groups', + name: 'my-group', + }, + { + kind: 'Location', + namespace: 'default', + name: 'my-location', + }, + { + kind: 'System', + namespace: 'default', + name: 'my-system', + }, + { + kind: 'User', + namespace: 'users', + name: 'my-api', + }, + ], + }, + { + target: 'https://my-location-2', + entities: [ + { + kind: 'API', + namespace: 'default', + name: 'my-api', + }, + { + kind: 'Component', + namespace: 'default', + name: 'my-component', + }, + { + kind: 'Location', + namespace: 'default', + name: 'my-location', + }, + ], + }, +]; + +createDevApp() + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + getEntities: async () => { + await new Promise(r => setTimeout(r, 1000)); + + return { + items: [ + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Group A', + }, + }, + }, + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-b', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Group B', + }, + }, + }, + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'other', + }, + spec: { + profile: { + displayName: 'Group A', + }, + }, + }, + ] as Entity[], + }; + }, + + addLocation: async location => { + await new Promise(r => setTimeout(r, 1000)); + + return { + location, + entities: getEntities(location.target), + }; + }, + } as CatalogApi), + }) + .registerApi({ + api: catalogImportApiRef, + deps: {}, + factory: () => + ({ + analyzeUrl: async (url: string): Promise => { + await new Promise(r => setTimeout(r, 500)); + + switch (url) { + case 'https://0': + return { + type: 'repository', + url, + integrationType: 'github', + generatedEntities: getEntities(url), + }; + + case 'https://1': + case 'https://2/catalog-info.yaml': + case 'https://2/folder-a/catalog-info.yaml': + case 'https://2/folder-b/catalog-info.yaml': + return { + type: 'locations', + locations: [ + { + target: url.includes('/catalog-info.yaml') + ? url + : `${url}/catalog-info.yaml`, + entities: getEntityNames(url), + }, + ], + }; + + case 'https://2': { + const urls = [ + `${url}/catalog-info.yaml`, + `${url}/folder-a/catalog-info.yaml`, + `${url}/folder-b/catalog-info.yaml`, + ]; + + return { + type: 'locations', + locations: urls.map(u => ({ + target: u, + entities: getEntityNames(u), + })), + }; + } + + default: + throw new Error(`Invalid url ${url}`); + } + }, + + submitPullRequest: async ({ + repositoryUrl, + }): Promise<{ + link: string; + location: string; + }> => { + await new Promise(r => setTimeout(r, 2500)); + + return { + link: `${repositoryUrl}/pulls/1`, + location: `${repositoryUrl}/blob/catalog-info.yaml`, + }; + }, + } as CatalogImportApi), + }) + .addPage({ + title: 'Catalog Import', + element: , + }) + .addPage({ + title: 'Catalog Import 2', + element: ( + + + + + + + + + + + + + + + + + + + + + + + ), + }) + .addPage({ + title: 'Components', + element: ( + + + + + + + } + /> + + + + + + + + + + + } + locations={locations} + locationListItemIcon={() => } + onItemClick={() => {}} + /> + + + + + } + /> + + + + + } + withLinks + /> + + + + + + ), + }) + .render(); diff --git a/plugins/catalog-import/docs/catalog-import-screenshot.png b/plugins/catalog-import/docs/catalog-import-screenshot.png new file mode 100644 index 0000000000..eb8348b7ef Binary files /dev/null and b/plugins/catalog-import/docs/catalog-import-screenshot.png differ diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 2b2b016716..3016ada247 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.3.7", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,16 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/integration": "^0.3.2", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/catalog-client": "^0.3.6", + "@backstage/core": "^0.6.1", + "@backstage/integration": "^0.4.0", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.12", + "@types/react": "^16.9", "git-url-parse": "^11.4.4", "react": "^16.13.1", "react-dom": "^16.13.1", @@ -49,12 +51,12 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/catalog-client": "^0.3.5", "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", + "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 5abb0e3e53..5237e0339f 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -14,29 +14,38 @@ * limitations under the License. */ +import { EntityName } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core'; -import { PartialEntity } from '../util/types'; -import { GitHubIntegrationConfig } from '@backstage/integration'; +import { PartialEntity } from '../types'; export const catalogImportApiRef = createApiRef({ id: 'plugin.catalog-import.service', description: 'Used by the catalog import plugin to make requests', }); +// result of the analyze state +export type AnalyzeResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + generatedEntities: PartialEntity[]; + }; + export interface CatalogImportApi { - submitPrToRepo(options: { - owner: string; - repo: string; + analyzeUrl(url: string): Promise; + + submitPullRequest(options: { + repositoryUrl: string; fileContent: string; - githubIntegrationConfig: GitHubIntegrationConfig; + title: string; + body: string; }): Promise<{ link: string; location: string }>; - checkForExistingCatalogInfo(options: { - owner: string; - repo: string; - githubIntegrationConfig: GitHubIntegrationConfig; - }): Promise<{ exists: boolean; url?: string }>; - createRepositoryLocation(options: { location: string }): Promise; - generateEntityDefinitions(options: { - repo: string; - }): Promise; } diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 0e0cf4b323..95082829c4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -14,51 +14,331 @@ * limitations under the License. */ -import { CatalogImportClient } from './CatalogImportClient'; +const octokit = { + repos: { + get: () => Promise.resolve({ data: { default_branch: 'main' } }), + createOrUpdateFileContents: jest.fn().mockImplementation(async () => {}), + }, + search: { + code: jest.fn(), + }, + git: { + getRef: async () => ({ + data: { object: { sha: 'any' } }, + }), + createRef: jest.fn().mockImplementation(async () => {}), + }, + pulls: { + create: jest.fn().mockImplementation(async () => ({ + data: { + html_url: 'http://pull/request/0', + }, + })), + }, +}; -jest.mock('@octokit/rest', () => ({ - Octokit: jest.fn().mockImplementation(() => { - return { - repos: { - get: () => - Promise.resolve({ - data: { - default_branch: 'main', - }, - }), - }, - search: { - code: () => - Promise.resolve({ - data: { - total_count: 2, - items: [ - { path: 'simple/path/catalog-info.yaml' }, - { path: 'co/mple/x/path/catalog-info.yaml' }, - { path: 'catalog-info.yaml' }, - ], - }, - }), - }, - }; - }), +jest.doMock('@octokit/rest', () => { + class Octokit { + constructor() { + return octokit; + } + } + return { Octokit }; +}); + +// Mock the value to control which integrations are activated +jest.mock('./GitHub', () => ({ + getGithubIntegrationConfig: jest.fn(), })); +import { ConfigReader, OAuthApi, UrlPatternDiscovery } from '@backstage/core'; +import { GitHubIntegrationConfig } from '@backstage/integration'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { msw } from '@backstage/test-utils'; +import { Octokit } from '@octokit/rest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { CatalogImportClient } from './CatalogImportClient'; +import { getGithubIntegrationConfig } from './GitHub'; + +const server = setupServer(); + describe('CatalogImportClient', () => { - describe('checkForExistingCatalogInfo', () => { - const cic = new CatalogImportClient({ - discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, - githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') }, - configApi: {} as any, + msw.setupDefaultHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/catalog'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + + const githubAuthApi: jest.Mocked = { + getAccessToken: jest.fn(), + }; + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; + + const configApi = new ConfigReader({}); + + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + addLocation: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + + let catalogImportClient: CatalogImportClient; + let getGithubIntegrationConfigFn: jest.Mock; + + beforeEach(() => { + catalogImportClient = new CatalogImportClient({ + discoveryApi, + githubAuthApi, + configApi, + identityApi, + catalogApi, }); - it('should return the closest-to-root catalog-info from multiple responses', async () => { - const respo = await cic.checkForExistingCatalogInfo({ - owner: 'test-user', - repo: 'rest-repo', - githubIntegrationConfig: { host: 'https://github.com' }, + + getGithubIntegrationConfigFn = getGithubIntegrationConfig as jest.Mock; + getGithubIntegrationConfigFn.mockReset(); + }); + + describe('analyzeUrl', () => { + it('should add yaml location', async () => { + catalogApi.addLocation.mockResolvedValueOnce({ + location: { + id: 'id-0', + type: 'url', + target: 'http://example.com/folder/catalog-info.yaml', + }, + entities: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-entity', + namespace: 'my-namespace', + }, + }, + ], + }); + + await expect( + catalogImportClient.analyzeUrl( + 'http://example.com/folder/catalog-info.yaml', + ), + ).resolves.toEqual({ + locations: [ + { + entities: [ + { + kind: 'Component', + name: 'my-entity', + namespace: 'my-namespace', + }, + ], + target: 'http://example.com/folder/catalog-info.yaml', + }, + ], + type: 'locations', + }); + + expect(catalogApi.addLocation).toBeCalledTimes(1); + expect(catalogApi.addLocation.mock.calls[0][0]).toEqual({ + type: 'url', + target: 'http://example.com/folder/catalog-info.yaml', + dryRun: true, + }); + }); + + it('should ignore missing github integration', async () => { + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).rejects.toThrow(new Error('Invalid url')); + }); + + it('should find locations from github', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: {} as GitHubIntegrationConfig, + }); + + ((new Octokit().search.code as any) as jest.Mock).mockResolvedValueOnce({ + data: { + total_count: 2, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }); + + catalogApi.addLocation.mockImplementation(async ({ type, target }) => ({ + location: { + id: 'id-0', + type: type ?? 'url', + target, + }, + entities: [ + { + apiVersion: '1', + kind: 'k', + metadata: { + name: 'e', + namespace: 'n', + }, + }, + ], + })); + + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).resolves.toEqual({ + locations: [ + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/simple/path/catalog-info.yaml', + }, + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/co/mple/x/path/catalog-info.yaml', + }, + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/catalog-info.yaml', + }, + ], + type: 'locations', + }); + }); + + it('should find repository from github', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: {} as GitHubIntegrationConfig, + }); + + ((new Octokit().search.code as any) as jest.Mock).mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }); + + server.use( + rest.post(`${mockBaseUrl}/analyze-location`, (req, res, ctx) => { + expect(req.body).toEqual({ + location: { + target: 'https://github.com/backstage/backstage', + type: 'url', + }, + }); + + return res( + ctx.json({ + generateEntities: [ + { + entity: { + kind: 'k', + metadata: { name: 'e', namespace: 'n' }, + }, + fields: [], + }, + ], + existingEntityFiles: [], + }), + ); + }), + ); + + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).resolves.toEqual({ + type: 'repository', + url: 'https://github.com/backstage/backstage', + integrationType: 'github', + generatedEntities: [ + { + kind: 'k', + metadata: { name: 'e', namespace: 'n' }, + }, + ], + }); + }); + }); + + describe('submitPullRequest', () => { + it('should create GitHub pull request', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: { + host: 'github.com', + } as GitHubIntegrationConfig, + }); + + await expect( + catalogImportClient.submitPullRequest({ + repositoryUrl: 'https://github.com/backstage/backstage', + fileContent: 'some content', + title: 'A title', + body: 'A body', + }), + ).resolves.toEqual({ + link: 'http://pull/request/0', + location: + 'https://github.com/backstage/backstage/blob/main/catalog-info.yaml', + }); + + expect( + ((new Octokit().git.createRef as any) as jest.Mock).mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + ref: 'refs/heads/backstage-integration', + sha: 'any', + }); + expect( + ((new Octokit().repos.createOrUpdateFileContents as any) as jest.Mock) + .mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + path: 'catalog-info.yaml', + message: 'Add catalog-info.yaml config file', + content: 'c29tZSBjb250ZW50', + branch: 'backstage-integration', + }); + expect( + ((new Octokit().pulls.create as any) as jest.Mock).mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + title: 'A title', + head: 'backstage-integration', + body: 'A body', + base: 'main', }); - expect(respo.exists).toBe(true); - expect(respo.url).toBe('blob/main/catalog-info.yaml'); }); }); }); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index d1509c2105..b4b1ebe845 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -14,37 +14,131 @@ * limitations under the License. */ -import { Octokit } from '@octokit/rest'; -import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core'; -import { CatalogImportApi } from './CatalogImportApi'; -import { PartialEntity } from '../util/types'; +import { CatalogApi } from '@backstage/catalog-client'; +import { EntityName } from '@backstage/catalog-model'; +import { + ConfigApi, + DiscoveryApi, + IdentityApi, + OAuthApi, +} from '@backstage/core'; import { GitHubIntegrationConfig } from '@backstage/integration'; +import { Octokit } from '@octokit/rest'; +import { PartialEntity } from '../types'; +import { AnalyzeResult, CatalogImportApi } from './CatalogImportApi'; +import { getGithubIntegrationConfig } from './GitHub'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; private readonly githubAuthApi: OAuthApi; private readonly configApi: ConfigApi; + private readonly catalogApi: CatalogApi; constructor(options: { discoveryApi: DiscoveryApi; githubAuthApi: OAuthApi; + identityApi: IdentityApi; configApi: ConfigApi; + catalogApi: CatalogApi; }) { this.discoveryApi = options.discoveryApi; this.githubAuthApi = options.githubAuthApi; + this.identityApi = options.identityApi; this.configApi = options.configApi; + this.catalogApi = options.catalogApi; } - async generateEntityDefinitions({ + async analyzeUrl(url: string): Promise { + if (url.match(/\.ya?ml$/)) { + const location = await this.catalogApi.addLocation({ + type: 'url', + target: url, + dryRun: true, + }); + + return { + type: 'locations', + locations: [ + { + target: location.location.target, + entities: location.entities.map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace ?? 'default', + name: e.metadata.name, + })), + }, + ], + }; + } + + const ghConfig = getGithubIntegrationConfig(this.configApi, url); + + if (ghConfig) { + // TODO: this could be part of the analyze-location endpoint + const locations = await this.checkGitHubForExistingCatalogInfo({ + ...ghConfig, + url, + }); + + if (locations.length > 0) { + return { + type: 'locations', + locations, + }; + } + + return { + type: 'repository', + integrationType: 'github', + url: url, + generatedEntities: await this.generateEntityDefinitions({ + repo: url, + }), + }; + } + + throw new Error('Invalid url'); + } + + async submitPullRequest({ + repositoryUrl, + fileContent, + title, + body, + }: { + repositoryUrl: string; + fileContent: string; + title: string; + body: string; + }): Promise<{ link: string; location: string }> { + const ghConfig = getGithubIntegrationConfig(this.configApi, repositoryUrl); + + if (ghConfig) { + return await this.submitGitHubPrToRepo({ + ...ghConfig, + fileContent, + title, + body, + }); + } + + throw new Error('unimplemented!'); + } + + // TODO: this could be part of the catalog api + private async generateEntityDefinitions({ repo, }: { repo: string; }): Promise { + const idToken = await this.identityApi.getIdToken(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, { headers: { 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), }, method: 'POST', body: JSON.stringify({ @@ -64,41 +158,23 @@ export class CatalogImportClient implements CatalogImportApi { return payload.generateEntities.map((x: any) => x.entity); } - async createRepositoryLocation({ - location, - }: { - location: string; - }): Promise { - const response = await fetch( - `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, - { - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - type: 'url', - target: location, - presence: 'optional', - }), - }, - ); - if (!response.ok) { - throw new Error( - `Received http response ${response.status}: ${response.statusText}`, - ); - } - } - - async checkForExistingCatalogInfo({ + // TODO: this response should better be part of the analyze-locations response and scm-independent / implemented per scm + private async checkGitHubForExistingCatalogInfo({ + url, owner, repo, githubIntegrationConfig, }: { + url: string; owner: string; repo: string; githubIntegrationConfig: GitHubIntegrationConfig; - }): Promise<{ exists: boolean; url?: string }> { + }): Promise< + Array<{ + target: string; + entities: EntityName[]; + }> + > { const token = await this.githubAuthApi.getAccessToken(['repo']); const octo = new Octokit({ auth: token, @@ -122,28 +198,50 @@ export class CatalogImportClient implements CatalogImportApi { }); const defaultBranch = repoInformation.data.default_branch; - // Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item', - // aka magic. - // Sorting to use the shortest item, closest to the repository root. - const catalogInfoItem = searchResult.data.items - .map(it => it.path) - .sort((a, b) => a.length - b.length)[0]; - return { - url: `blob/${defaultBranch}/${catalogInfoItem}`, - exists, - }; + return await Promise.all( + searchResult.data.items + .map( + i => `${url.replace(/[\/]*$/, '')}/blob/${defaultBranch}/${i.path}`, + ) + .map( + async i => + ({ + target: i, + entities: ( + await this.catalogApi.addLocation({ + type: 'url', + target: i, + dryRun: true, + }) + ).entities.map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace ?? 'default', + name: e.metadata.name, + })), + } as { + target: string; + entities: EntityName[]; + }), + ), + ); } - return { exists }; + + return []; } - async submitPrToRepo({ + // TODO: extract this function and implement for non-github + private async submitGitHubPrToRepo({ owner, repo, + title, + body, fileContent, githubIntegrationConfig, }: { owner: string; repo: string; + title: string; + body: string; fileContent: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }> { @@ -212,23 +310,13 @@ export class CatalogImportClient implements CatalogImportApi { ); }); - const appTitle = - this.configApi.getOptionalString('app.title') ?? 'Backstage'; - const appBaseUrl = this.configApi.getString('app.baseUrl'); - - const prBody = `This pull request adds a **Backstage entity metadata file** \ -to this repository so that the component can be added to the \ -[${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ -the component will become available.\n\nFor more information, read an \ -[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`; - const pullRequestResponse = await octo.pulls .create({ owner, repo, - title: `Add ${fileName} config file`, + title, head: branchName, - body: prBody, + body, base: repoData.data.default_branch, }) .catch(e => { diff --git a/plugins/catalog-import/src/api/GitHub.ts b/plugins/catalog-import/src/api/GitHub.ts new file mode 100644 index 0000000000..6a798ec201 --- /dev/null +++ b/plugins/catalog-import/src/api/GitHub.ts @@ -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 { ConfigApi } from '@backstage/core'; +import { ScmIntegrations } from '@backstage/integration'; +import parseGitUrl from 'git-url-parse'; + +export const getGithubIntegrationConfig = ( + config: ConfigApi, + location: string, +) => { + const { name: repo, owner } = parseGitUrl(location); + + const scmIntegrations = ScmIntegrations.fromConfig(config); + const githubIntegrationConfig = scmIntegrations.github.byUrl(location); + + if (!githubIntegrationConfig) { + return undefined; + } + + return { + repo, + owner, + githubIntegrationConfig: githubIntegrationConfig.config, + }; +}; diff --git a/plugins/catalog-import/src/assets/catalog-import-screenshot.png b/plugins/catalog-import/src/assets/catalog-import-screenshot.png deleted file mode 100644 index 7ba8ae9eb5..0000000000 Binary files a/plugins/catalog-import/src/assets/catalog-import-screenshot.png and /dev/null differ diff --git a/plugins/catalog-import/src/components/Buttons/index.tsx b/plugins/catalog-import/src/components/Buttons/index.tsx new file mode 100644 index 0000000000..93dd96b399 --- /dev/null +++ b/plugins/catalog-import/src/components/Buttons/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2021 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, CircularProgress } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { ComponentProps } from 'react'; + +const useStyles = makeStyles(theme => ({ + wrapper: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + position: 'relative', + }, + buttonProgress: { + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, + button: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + }, +})); + +export const NextButton = ( + props: ComponentProps & { loading?: boolean }, +) => { + const { loading, ...buttonProps } = props; + const classes = useStyles(); + + return ( + + + {props.loading && ( + + )} + {props.loading} + + ); +}; + +export const BackButton = (props: ComponentProps) => { + const classes = useStyles(); + + return ( + + {props.children || 'Back'} + + ); +}; diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx deleted file mode 100644 index 9fb170c85c..0000000000 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ /dev/null @@ -1,200 +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 { Entity } from '@backstage/catalog-model'; -import { - errorApiRef, - RouteRef, - StructuredMetadataTable, - useApi, -} from '@backstage/core'; -import { - entityRoute, - entityRouteParams, -} from '@backstage/plugin-catalog-react'; -import { - Button, - CircularProgress, - Divider, - Grid, - Link, - List, - ListItem, - Typography, -} from '@material-ui/core'; -import React, { useCallback, useState } from 'react'; -import { generatePath, resolvePath } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; -import * as YAML from 'yaml'; -import { PartialEntity } from '../util/types'; -import { urlType } from '../util/urls'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ConfigSpec } from './ImportComponentPage'; - -const getEntityCatalogPath = ({ - entity, - catalogRouteRef, -}: { - entity: PartialEntity; - catalogRouteRef: RouteRef; -}) => { - const relativeEntityPathInsideCatalog = generatePath( - entityRoute.path, - entityRouteParams(entity as Entity), - ); - - const resolvedAbsolutePath = resolvePath( - relativeEntityPathInsideCatalog, - catalogRouteRef.path, - )?.pathname; - - return resolvedAbsolutePath; -}; - -type Props = { - nextStep: (options?: { reset: boolean }) => void; - configFile: ConfigSpec; - savePRLink: (PRLink: string) => void; - catalogRouteRef: RouteRef; -}; - -const ComponentConfigDisplay = ({ - nextStep, - configFile, - savePRLink, - catalogRouteRef, -}: Props) => { - const [errorOccurred, setErrorOccurred] = useState(false); - const [submitting, setSubmitting] = useState(false); - const errorApi = useApi(errorApiRef); - const { submitPrToRepo, addLocation } = useGithubRepos(); - const onNext = useCallback(async () => { - try { - setSubmitting(true); - if (urlType(configFile.location) === 'tree') { - const result = await submitPrToRepo(configFile); - savePRLink(result.link); - setSubmitting(false); - nextStep(); - } else { - addLocation(configFile.location); - setSubmitting(false); - nextStep(); - } - } catch (e) { - setErrorOccurred(true); - setSubmitting(false); - errorApi.post(e); - } - }, [submitPrToRepo, configFile, nextStep, savePRLink, errorApi, addLocation]); - - return ( - - {urlType(configFile.location) === 'tree' ? ( - - Following config object will be submitted in a pull request to the - repository{' '} - - {configFile.location} - {' '} - and added as a new location to the backend - - ) : ( - - Following config object will be added as a new location to the backend{' '} - - {configFile.location} - - - )} - - - {urlType(configFile.location) === 'tree' ? ( - {YAML.stringify(configFile.config)} - ) : ( - - {configFile.config.map((entity: any, index: number) => { - const entityPath = getEntityCatalogPath({ - entity, - catalogRouteRef, - }); - return ( - - - - {entityPath} - - ), - }} - /> - - {index < configFile.config.length - 1 && ( - - )} - - ); - })} - - )} - - - {submitting ? ( - - - - ) : null} - - - Next - - {errorOccurred ? ( - nextStep({ reset: true })} - > - Start again - - ) : null} - - - - ); -}; - -export default ComponentConfigDisplay; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx new file mode 100644 index 0000000000..d94a1cad19 --- /dev/null +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -0,0 +1,165 @@ +/* + * Copyright 2021 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, EntityName } from '@backstage/catalog-model'; +import { + EntityRefLink, + formatEntityRefTitle, +} from '@backstage/plugin-catalog-react'; +import { + Collapse, + IconButton, + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import ApartmentIcon from '@material-ui/icons/Apartment'; +import CategoryIcon from '@material-ui/icons/Category'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import GroupIcon from '@material-ui/icons/Group'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import MemoryIcon from '@material-ui/icons/Memory'; +import PersonIcon from '@material-ui/icons/Person'; +import WorkIcon from '@material-ui/icons/Work'; +import React, { useState } from 'react'; + +const useStyles = makeStyles(theme => ({ + nested: { + paddingLeft: theme.spacing(4), + }, +})); + +function sortEntities(entities: Array) { + return entities.sort((a, b) => + formatEntityRefTitle(a).localeCompare(formatEntityRefTitle(b)), + ); +} + +function getEntityIcon(entity: { kind: string }): React.ReactElement { + switch (entity.kind.toLowerCase()) { + case 'api': + return ; + + case 'component': + return ; + + case 'domain': + return ; + + case 'group': + return ; + + case 'location': + return ; + + case 'system': + return ; + + case 'user': + return ; + + default: + return ; + } +} + +type Props = { + locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; + locationListItemIcon: (target: string) => React.ReactElement; + collapsed?: boolean; + firstListItem?: React.ReactElement; + onItemClick?: (target: string) => void; + withLinks?: boolean; +}; + +export const EntityListComponent = ({ + locations, + collapsed = false, + locationListItemIcon, + onItemClick, + firstListItem, + withLinks = false, +}: Props) => { + const classes = useStyles(); + + const [expandedUrls, setExpandedUrls] = useState([]); + + const handleClick = (url: string) => { + setExpandedUrls(urls => + urls.includes(url) ? urls.filter(u => u !== url) : urls.concat(url), + ); + }; + + return ( + + {firstListItem} + {locations.map(r => ( + + onItemClick?.call(this, r.target)} + > + {locationListItemIcon(r.target)} + + + + {collapsed && ( + + handleClick(r.target)}> + {expandedUrls.includes(r.target) ? ( + + ) : ( + + )} + + + )} + + + + + {sortEntities(r.entities).map(entity => ( + + {getEntityIcon(entity)} + + + ))} + + + + ))} + + ); +}; diff --git a/plugins/catalog-import/src/components/EntityListComponent/index.ts b/plugins/catalog-import/src/components/EntityListComponent/index.ts new file mode 100644 index 0000000000..fc0efc5481 --- /dev/null +++ b/plugins/catalog-import/src/components/EntityListComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { EntityListComponent } from './EntityListComponent'; diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx deleted file mode 100644 index 38b1065325..0000000000 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2021 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 { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - DiscoveryApi, - errorApiRef, -} from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import React from 'react'; -import { catalogImportApiRef, CatalogImportClient } from '../api'; -import { RegisterComponentForm } from './ImportComponentForm'; - -describe('', () => { - let apis: ApiRegistry; - - const mockErrorApi: jest.Mocked = { - post: jest.fn(), - error$: jest.fn(), - }; - - beforeEach(() => { - apis = ApiRegistry.from([ - [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })], - [ - catalogImportApiRef, - new CatalogImportClient({ - discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, - githubAuthApi: { - getAccessToken: (_, __) => Promise.resolve('token'), - }, - configApi: {} as any, - }), - ], - [errorApiRef, mockErrorApi], - ]); - }); - - async function renderSUT( - nextStep: () => void = () => {}, - saveConfig: () => void = () => {}, - ) { - return await renderInTestApp( - - - , - ); - } - - it('Renders without exploding', async () => { - await renderSUT(); - expect( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - ).toBeInTheDocument(); - }); - - it('Should have basic URL validation for input', async () => { - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: 'not a url' } }, - ); - }); - await waitFor(() => { - fireEvent.click(screen.getByText('Next')); - }); - expect(screen.getByText('Must start with https://.')).toBeInTheDocument(); - }); -}); diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx deleted file mode 100644 index b3ec050ea3..0000000000 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ /dev/null @@ -1,154 +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 { errorApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { BackstageTheme } from '@backstage/theme'; -import { - Button, - FormControl, - FormHelperText, - TextField, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; -import { useForm } from 'react-hook-form'; -import { useMountedState } from 'react-use'; -import { urlType } from '../util/urls'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ComponentIdValidators } from '../util/validate'; -import { ConfigSpec } from './ImportComponentPage'; - -const useStyles = makeStyles(theme => ({ - form: { - alignItems: 'flex-start', - display: 'flex', - flexFlow: 'column nowrap', - }, - submit: { - marginTop: theme.spacing(1), - }, -})); - -type Props = { - nextStep: () => void; - saveConfig: (configFile: ConfigSpec) => void; - repository: string; -}; - -export const RegisterComponentForm = ({ - nextStep, - saveConfig, - repository, -}: Props) => { - const { register, handleSubmit, errors, formState } = useForm({ - mode: 'onChange', - }); - const classes = useStyles(); - const hasErrors = !!errors.componentLocation; - const dirty = formState?.isDirty; - const catalogApi = useApi(catalogApiRef); - - const isMounted = useMountedState(); - const errorApi = useApi(errorApiRef); - const { - generateEntityDefinitions, - checkForExistingCatalogInfo, - } = useGithubRepos(); - - const onSubmit = async (formData: Record) => { - const { componentLocation: target } = formData; - async function saveCatalogFileConfig(targetString: string) { - const data = await catalogApi.addLocation({ target: targetString }); - saveConfig({ - type: 'file', - location: data.location.target, - config: data.entities, - }); - } - - async function trySaveRepositoryConfig(targetString: string) { - const existingCatalog = await checkForExistingCatalogInfo(targetString); - if (existingCatalog.exists) { - const targetUrl = targetString.endsWith('/') - ? `${targetString}${existingCatalog.url}` - : `${targetString}/${existingCatalog.url}`; - await saveCatalogFileConfig(targetUrl); - } else { - saveConfig({ - type: 'tree', - location: target, - config: await generateEntityDefinitions(target), - }); - } - } - - try { - if (!isMounted()) return; - const type = urlType(target); - if (type === 'tree') { - await trySaveRepositoryConfig(target); - } else { - await saveCatalogFileConfig(target); - } - nextStep(); - } catch (e) { - errorApi.post(e); - } - }; - - return ( - - - - - {errors.componentLocation && ( - - {errors.componentLocation.message} - - )} - - - - Next - - - ); -}; diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index f6ef07e1b2..4cadea85c0 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -13,254 +13,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CatalogClient } from '@backstage/catalog-client'; import { ApiProvider, ApiRegistry, configApiRef, - errorApiRef, + ConfigReader, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { msw, renderInTestApp } from '@backstage/test-utils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { act, render } from '@testing-library/react'; import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../api'; import { ImportComponentPage } from './ImportComponentPage'; -let codeSearchMockResponse: () => Promise<{ - data: { - total_count: number; - items: Array<{ path: string }>; - }; -}>; - -let findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', -}); - -jest.mock('@backstage/integration', () => ({ - readGitHubIntegrationConfigs: () => ({ - find: findGithubConfigMockResponse, - }), -})); - -jest.mock('@octokit/rest', () => ({ - Octokit: jest.fn().mockImplementation(() => { - return { - repos: { - get: () => - Promise.resolve({ - data: { - default_branch: 'main', - }, - }), - }, - search: { - code: codeSearchMockResponse, - }, - }; - }), -})); - -const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo'; describe('', () => { - const server = setupServer(); - msw.setupDefaultHandlers(server); - - beforeEach(() => { - server.use( - rest.post('https://backend.localhost/locations', (_, res, ctx) => { - return res( - ctx.status(201), - ctx.json(require('../mocks/locations-POST-response.json')), - ); - }), - rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => { - return res( - ctx.json(require('../mocks/analyze-location-POST-response.json')), - ); - }), - ); - }); - beforeAll(() => server.listen()); - afterEach(() => server.resetHandlers()); - afterAll(() => server.close()); + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; let apis: ApiRegistry; - const mockErrorApi: jest.Mocked = { - post: jest.fn(), - error$: jest.fn(), - }; - beforeEach(() => { - const getBaseUrl = () => Promise.resolve('https://backend.localhost'); - apis = ApiRegistry.from([ - [ - catalogApiRef, - new CatalogClient({ - discoveryApi: { getBaseUrl }, - }), - ], - [ + apis = ApiRegistry.with( + configApiRef, + new ConfigReader({ integrations: {} }), + ) + .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) + .with( catalogImportApiRef, new CatalogImportClient({ - discoveryApi: { getBaseUrl }, + discoveryApi: {} as any, githubAuthApi: { - getAccessToken: (_, __) => Promise.resolve('token'), + getAccessToken: async () => 'token', }, + identityApi, configApi: {} as any, + catalogApi: {} as any, }), - ], - [ - configApiRef, - { - getOptional: () => 'Title', - getOptionalConfigArray: () => [], - has: () => true, - getConfig: () => ({ - has: () => true, - }), - }, - ], - [errorApiRef, mockErrorApi], - ]); + ); }); - async function renderSUT() { - return await renderInTestApp( - - - , - ); - } - - it('Should not explode on non-Github URLs', async () => { - findGithubConfigMockResponse = () => undefined!!; - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { - target: { - value: 'https://test-git-provider.localhost/someuser/somerepo', - }, - }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - const firstStepInput = screen.queryByPlaceholderText( - 'https://github.com/backstage/backstage', - ); - expect(firstStepInput).not.toBeInTheDocument(); - }); - }); - - it('Should offer direct file import from non-Github URLs', async () => { - findGithubConfigMockResponse = () => undefined!!; - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { - target: { - value: - 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', - }, - }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - const firstStepInput = screen.queryByPlaceholderText( - 'https://github.com/backstage/backstage', - ); - expect(firstStepInput).not.toBeInTheDocument(); - }); - expect( - screen.getByText( - 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', - ), - ).toBeInTheDocument(); - }); - - it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => { - findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', - }); - codeSearchMockResponse = () => - Promise.resolve({ - data: { - total_count: 3, - items: [ - { path: 'simple/path/catalog-info.yaml' }, - { path: 'co/mple/x/path/catalog-info.yaml' }, - { path: 'catalog-info.yaml' }, - ], - }, - }); - await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: OUR_GITHUB_TEST_REPO } }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - expect( - screen.getByText( - 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', + it('renders without exploding', async () => { + await act(async () => { + const { getByText } = render( + wrapInTestApp( + + + , ), - ).toBeInTheDocument(); - - const pullReqText = screen.queryByText('pull request'); - expect(pullReqText).not.toBeInTheDocument(); - }); - }); - - it('Should indicate a pull request creation when no yaml file found in the repo', async () => { - findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', - }); - codeSearchMockResponse = () => - Promise.resolve({ - data: { - total_count: 0, - items: [], - }, - }); - const { container } = await renderSUT(); - await waitFor(() => { - fireEvent.input( - screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: OUR_GITHUB_TEST_REPO } }, ); - }); - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument(); + expect( + await getByText('Start tracking your component in Backstage'), + ).toBeInTheDocument(); }); - const textNode = container - .querySelector('a[href="https://github.com/someuser/somerepo"]') - ?.closest('p'); - expect(textNode?.innerHTML).toContain( - 'Following config object will be submitted in a pull request to the repository', - ); - expect( - screen.queryByText( - 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', - ), - ).not.toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index adae137a50..1d1a8056bd 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -14,35 +14,21 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Grid, Typography } from '@material-ui/core'; import { + ConfigApi, + configApiRef, + Content, + ContentHeader, + Header, InfoCard, Page, - Content, - Header, SupportButton, - ContentHeader, - RouteRef, useApi, - configApiRef, - ConfigApi, } from '@backstage/core'; -import { RegisterComponentForm } from './ImportComponentForm'; -import ImportStepper from './ImportStepper'; -import ComponentConfigDisplay from './ComponentConfigDisplay'; -import { ImportFinished } from './ImportFinished'; -import { PartialEntity } from '../util/types'; - -export type ConfigSpec = { - type: 'tree' | 'file'; - location: string; - config: PartialEntity[]; -}; - -function manifestGenerationAvailable(configApi: ConfigApi): boolean { - return configApi.has('integrations.github'); -} +import { Grid, Typography } from '@material-ui/core'; +import React from 'react'; +import { ImportStepper } from './ImportStepper'; +import { StepperProviderOpts } from './ImportStepper/defaults'; function repositories(configApi: ConfigApi): string[] { const integrations = configApi.getConfig('integrations'); @@ -62,26 +48,13 @@ function repositories(configApi: ConfigApi): string[] { return repos; } -export const ImportComponentPage = ({ - catalogRouteRef, -}: { - catalogRouteRef: RouteRef; -}) => { - const [activeStep, setActiveStep] = useState(0); - const [configFile, setConfigFile] = useState({ - type: 'tree', - location: '', - config: [], - }); - const [endLink, setEndLink] = useState(''); - const nextStep = (options?: { reset: boolean }) => { - setActiveStep(step => (options?.reset ? 0 : step + 1)); - }; - +export const ImportComponentPage = (opts: StepperProviderOpts) => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; + const repos = repositories(configApi); const repositoryString = repos.join(', ').replace(/, (\w*)$/, ' or $1'); + return ( @@ -92,9 +65,11 @@ export const ImportComponentPage = ({ software catalog. - - + + + - Ways to register an existing component + Enter the URL to your SCM repository to add it to {appTitle}. - - {manifestGenerationAvailable(configApi) && ( - - GitHub Repo - - If you already have code in a GitHub repository without - Backstage metadata file set up for it, enter the full URL to - your repo and a new pull request with a sample Backstage - metadata Entity File (catalog-info.yaml) will - be opened for you. - - - )} - {repos.length === 1 ? `${repos[0]} ` : ''} Repository & - Entity File + Link to an existing entity file + + + Example:{' '} + + https://github.com/backstage/backstage/blob/master/catalog-info.yaml + - If you've already created a {appTitle} metadata file and put it - in your {repositoryString} repository, you can enter the full - URL to that Entity File. + The wizard analyzes the file, previews the entities, and adds + them to the {appTitle} catalog. + {repos.length > 0 && ( + <> + + Link to a {repositoryString} repository + + + Example: https://github.com/backstage/backstage + + + The wizard discovers all catalog-info.yaml{' '} + files in the repository, previews the entities, and adds + them to the {appTitle} catalog. + + {!opts?.pullRequest?.disable && ( + + If no entities are found, the wizard will prepare a Pull + Request that adds an example{' '} + catalog-info.yaml and prepares the {appTitle}{' '} + catalog to load all entities as soon as the Pull Request + is merged. + + )} + > + )} - - - - ), - }, - { - step: 'Review', - content: ( - - ), - }, - { - step: 'Finish', - content: ( - - ), - }, - ]} - activeStep={activeStep} - nextStep={nextStep} - /> - + + + diff --git a/plugins/catalog-import/src/components/ImportFinished.tsx b/plugins/catalog-import/src/components/ImportFinished.tsx deleted file mode 100644 index 9b0f8575a6..0000000000 --- a/plugins/catalog-import/src/components/ImportFinished.tsx +++ /dev/null @@ -1,58 +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 React from 'react'; -import { Alert } from '@material-ui/lab'; -import { Button, Grid, Link } from '@material-ui/core'; - -type Props = { - type: 'tree' | 'file'; - nextStep: (options?: { reset: boolean }) => void; - PRLink: string; -}; - -export const ImportFinished = ({ nextStep, PRLink, type }: Props) => { - return ( - - - - {type === 'tree' - ? 'Pull requests have been successfully opened. You can start again to import more repositories' - : 'Entity added to catalog successfully'} - - - - {type === 'tree' ? ( - - View pull request on GitHub - - ) : null} - nextStep({ reset: true })} - > - Register another - - - - ); -}; diff --git a/plugins/catalog-import/src/components/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper.tsx deleted file mode 100644 index 03752fa7d4..0000000000 --- a/plugins/catalog-import/src/components/ImportStepper.tsx +++ /dev/null @@ -1,42 +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 React from 'react'; -import Stepper from '@material-ui/core/Stepper'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/StepLabel'; -import { StepContent } from '@material-ui/core'; - -type Props = { - steps: { step: string; content: React.ReactNode }[]; - activeStep: number; - nextStep: (options?: { reset: boolean }) => void; -}; - -export default function ImportStepper({ steps, activeStep }: Props) { - return ( - - {steps.map(({ step }) => { - const stepProps: { completed?: boolean } = {}; - return ( - - {step} - {steps[activeStep].content} - - ); - })} - - ); -} diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx new file mode 100644 index 0000000000..f92f03c5cb --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2021 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 { configApiRef, InfoCard, useApi } from '@backstage/core'; +import { Step, StepContent, Stepper } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { useMemo } from 'react'; +import { ImportFlows, ImportState, useImportState } from '../useImportState'; +import { + defaultGenerateStepper, + defaultStepper, + StepConfiguration, + StepperProvider, + StepperProviderOpts, +} from './defaults'; + +const useStyles = makeStyles(() => ({ + stepperRoot: { + padding: 0, + }, +})); + +type Props = { + initialUrl?: string; + generateStepper?: ( + flow: ImportFlows, + defaults: StepperProvider, + ) => StepperProvider; + variant?: string; + opts?: StepperProviderOpts; +}; + +export const ImportStepper = ({ + initialUrl, + generateStepper = defaultGenerateStepper, + variant, + opts, +}: Props) => { + const configApi = useApi(configApiRef); + const classes = useStyles(); + const state = useImportState({ initialUrl }); + + const states = useMemo( + () => generateStepper(state.activeFlow, defaultStepper), + [generateStepper, state.activeFlow], + ); + + const render = (step: StepConfiguration) => { + return ( + + {step.stepLabel} + {step.content} + + ); + }; + + return ( + + + {render( + states.analyze( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + {render( + states.prepare( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + {render( + states.review( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + {render( + states.finish( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + + + ); +}; diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx new file mode 100644 index 0000000000..f2dd1d62f4 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -0,0 +1,293 @@ +/* + * Copyright 2021 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 { ConfigApi } from '@backstage/core'; +import { Box, StepLabel, TextField, Typography } from '@material-ui/core'; +import React from 'react'; +import { BackButton } from '../Buttons'; +import { StepFinishImportLocation } from '../StepFinishImportLocation'; +import { StepInitAnalyzeUrl } from '../StepInitAnalyzeUrl'; +import { + AutocompleteTextField, + StepPrepareCreatePullRequest, +} from '../StepPrepareCreatePullRequest'; +import { StepPrepareSelectLocations } from '../StepPrepareSelectLocations'; +import { StepReviewLocation } from '../StepReviewLocation'; +import { ImportFlows, ImportState } from '../useImportState'; + +export type StepperProviderOpts = { + pullRequest?: { + disable?: boolean; + preparePullRequest?: (apis: StepperApis) => { title: string; body: string }; + }; +}; + +type StepperApis = { + configApi: ConfigApi; +}; + +export type StepConfiguration = { + stepLabel: React.ReactElement; + content: React.ReactElement; +}; + +export type StepperProvider = { + analyze: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; + prepare: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; + review: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; + finish: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; +}; + +function defaultPreparePullRequest(apis: StepperApis) { + const appTitle = apis.configApi.getOptionalString('app.title') ?? 'Backstage'; + const appBaseUrl = apis.configApi.getString('app.baseUrl'); + + return { + title: 'Add catalog-info.yaml config file', + body: `This pull request adds a **Backstage entity metadata file** \ +to this repository so that the component can be added to the \ +[${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ +the component will become available.\n\nFor more information, read an \ +[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`, + }; +} + +/** + * The default stepper generation function. + * + * Override this function to customize the import flow. Each flow should at + * least override the prepare operation. + * + * @param flow the name of the active flow + * @param defaults the default steps + */ +export function defaultGenerateStepper( + flow: ImportFlows, + defaults: StepperProvider, +): StepperProvider { + switch (flow) { + // the prepare step is skipped but the label of the step is updated + case 'single-location': + return { + ...defaults, + prepare: () => ({ + stepLabel: ( + + Discovered Locations: 1 + + } + > + Select Locations + + ), + content: <>>, + }), + }; + + // let the user select one or more of the discovered locations in the prepare step + case 'multiple-locations': + return { + ...defaults, + prepare: (state, opts) => { + if (state.analyzeResult.type !== 'locations') { + return defaults.prepare(state, opts); + } + + return { + stepLabel: ( + + Discovered Locations: {state.analyzeResult.locations.length} + + } + > + Select Locations + + ), + content: ( + + ), + }; + }, + }; + + case 'no-location': + return { + ...defaults, + prepare: (state, opts) => { + if (state.analyzeResult.type !== 'repository') { + return defaults.prepare(state, opts); + } + + const { title, body } = ( + opts?.opts?.pullRequest?.preparePullRequest ?? + defaultPreparePullRequest + )(opts.apis); + + return { + stepLabel: Create Pull Request, + content: ( + ( + <> + + Pull Request Details + + + + + + + + Entity Configuration + + + + + + > + )} + /> + ), + }; + }, + }; + + default: + return defaults; + } +} + +export const defaultStepper: StepperProvider = { + analyze: (state, { opts }) => ({ + stepLabel: Select URL, + content: ( + + ), + }), + + prepare: state => ({ + stepLabel: ( + Optional}> + Import Actions + + ), + content: , + }), + + review: state => ({ + stepLabel: Review, + content: ( + + ), + }), + + finish: state => ({ + stepLabel: Finish, + content: ( + + ), + }), +}; diff --git a/plugins/catalog-import/src/components/ImportStepper/index.ts b/plugins/catalog-import/src/components/ImportStepper/index.ts new file mode 100644 index 0000000000..8d205562a4 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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 { ImportStepper } from './ImportStepper'; +export { defaultGenerateStepper } from './defaults'; diff --git a/plugins/catalog-import/src/components/Router.tsx b/plugins/catalog-import/src/components/Router.tsx index 5175f8f077..fc74c32b30 100644 --- a/plugins/catalog-import/src/components/Router.tsx +++ b/plugins/catalog-import/src/components/Router.tsx @@ -14,15 +14,13 @@ * limitations under the License. */ -import { RouteRef } from '@backstage/core'; import React from 'react'; import { Route, Routes } from 'react-router-dom'; import { ImportComponentPage } from './ImportComponentPage'; +import { StepperProviderOpts } from './ImportStepper/defaults'; -export const Router = ({ catalogRouteRef }: { catalogRouteRef: RouteRef }) => ( +export const Router = (opts: StepperProviderOpts) => ( - } - /> + } /> ); diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx new file mode 100644 index 0000000000..cc5eb9adef --- /dev/null +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2021 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 { Link } from '@backstage/core'; +import { Grid, Typography } from '@material-ui/core'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React from 'react'; +import { BackButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { ReviewResult } from '../useImportState'; + +type Props = { + reviewResult: ReviewResult; + onReset: () => void; +}; + +export const StepFinishImportLocation = ({ reviewResult, onReset }: Props) => ( + <> + {reviewResult.type === 'repository' && ( + <> + + The following Pull Request has been opened:{' '} + + {reviewResult.pullRequest.url} + + + + + Your entities will be imported as soon as the Pull Request is merged. + + > + )} + + + The following entities have been added to the catalog: + + + } + withLinks + /> + + + Register another + + > +); diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts b/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts new file mode 100644 index 0000000000..5f3d9f425c --- /dev/null +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { StepFinishImportLocation } from './StepFinishImportLocation'; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx new file mode 100644 index 0000000000..bfb2434926 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -0,0 +1,401 @@ +/* + * 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 { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult, catalogImportApiRef } from '../../api/'; +import { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; + +describe('', () => { + const catalogImportApi: jest.Mocked = { + analyzeUrl: jest.fn(), + submitPullRequest: jest.fn(), + }; + + const errorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const location = { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + const { getByRole } = render( + undefined} />, + { + wrapper: Wrapper, + }, + ); + + expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); + expect(getByRole('textbox', { name: /Repository/i })).toHaveValue(''); + }); + + it('should use default analysis url', async () => { + const { getByRole } = render( + undefined} + analysisUrl="https://default" + />, + { + wrapper: Wrapper, + }, + ); + + expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); + expect(getByRole('textbox', { name: /Repository/i })).toHaveValue( + 'https://default', + ); + }); + + it('should not analyze without url', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); + expect(onAnalysisFn).toBeCalledTimes(0); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze invalid value', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'http:/', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); + expect(onAnalysisFn).toBeCalledTimes(0); + expect(errorApi.post).toBeCalledTimes(0); + expect(getByText('Must start with https://.')).toBeInTheDocument(); + }); + + it('should analyze single location', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [location], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'single-location', + 'https://my-repository', + analyzeResult, + { prepareResult: analyzeResult }, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should analyze multiple locations', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [location, location], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-1', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'multiple-locations', + 'https://my-repository-1', + analyzeResult, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze with no locations', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-1', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText('There are no entities at this location'), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should analyze repository', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'component', + metadata: { + name: 'component-a', + }, + }, + ], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'no-location', + 'https://my-repository-2', + analyzeResult, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze repository without entities', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText("Couldn't generate entities for your repository"), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze repository if disabled', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'component', + metadata: { + name: 'component-a', + }, + }, + ], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText("Couldn't generate entities for your repository"), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should report unknown type to the errorapi', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(({ type: 'unknown' } as any) as AnalyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText( + 'Received unknown analysis result of type unknown. Please contact the support team.', + ), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(1); + expect(errorApi.post.mock.calls[0][0]).toMatchObject( + new Error( + 'Received unknown analysis result of type unknown. Please contact the support team.', + ), + ); + }); +}); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx new file mode 100644 index 0000000000..a33fe388d8 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2021 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 { FormHelperText, Grid, TextField } from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { NextButton } from '../Buttons'; +import { ImportFlows, PrepareResult } from '../useImportState'; + +type FormData = { + url: string; +}; + +type Props = { + onAnalysis: ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, + ) => void; + disablePullRequest?: boolean; + analysisUrl?: string; +}; + +/** + * A form that lets the user input a url and analyze it for existing locations or potential entities. + * + * @param onAnalysis is called when the analysis was successful + * @param analysisUrl a url that can be used as a default value + * @param disablePullRequest if true, repositories without entities will abort the wizard + */ +export const StepInitAnalyzeUrl = ({ + onAnalysis, + analysisUrl = '', + disablePullRequest = false, +}: Props) => { + const errorApi = useApi(errorApiRef); + const catalogImportApi = useApi(catalogImportApiRef); + + const { register, handleSubmit, errors, watch } = useForm({ + mode: 'onTouched', + defaultValues: { + url: analysisUrl, + }, + }); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(undefined); + + const handleResult = useCallback( + async ({ url }: FormData) => { + setSubmitted(true); + + try { + const analysisResult = await catalogImportApi.analyzeUrl(url); + + switch (analysisResult.type) { + case 'repository': + if ( + !disablePullRequest && + analysisResult.generatedEntities.length > 0 + ) { + onAnalysis('no-location', url, analysisResult); + } else { + setError("Couldn't generate entities for your repository"); + setSubmitted(false); + } + break; + + case 'locations': { + if (analysisResult.locations.length === 1) { + onAnalysis('single-location', url, analysisResult, { + prepareResult: analysisResult, + }); + } else if (analysisResult.locations.length > 1) { + onAnalysis('multiple-locations', url, analysisResult); + } else { + setError('There are no entities at this location'); + setSubmitted(false); + } + break; + } + + default: { + const err = `Received unknown analysis result of type ${ + (analysisResult as any).type + }. Please contact the support team.`; + setError(err); + setSubmitted(false); + + errorApi.post(new Error(err)); + break; + } + } + } catch (e) { + setError(e.message); + setSubmitted(false); + } + }, + [catalogImportApi, disablePullRequest, errorApi, onAnalysis], + ); + + return ( + + + (typeof value === 'string' && + value.match(/^https:\/\//) !== null) || + 'Must start with https://.', + }, + })} + required + /> + + {errors.url && ( + {errors.url.message} + )} + + {error && {error}} + + + + Analyze + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts new file mode 100644 index 0000000000..5d36c104c3 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx new file mode 100644 index 0000000000..3231c3c6a1 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2021 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 { CircularProgress, TextField } from '@material-ui/core'; +import { TextFieldProps } from '@material-ui/core/TextField/TextField'; +import { Autocomplete } from '@material-ui/lab'; +import React from 'react'; +import { + Control, + Controller, + FieldErrors, + ValidationRules, +} from 'react-hook-form'; + +type Props = { + name: TFieldValue; + options: string[]; + required?: boolean; + + control?: Control>; + errors?: FieldErrors>; + rules?: ValidationRules; + + loading?: boolean; + loadingText?: string; + + helperText?: React.ReactNode; + errorHelperText?: string; + + textFieldProps?: Omit; +}; + +export const AutocompleteTextField = ({ + name, + options, + required, + control, + errors, + rules, + loading = false, + loadingText, + helperText, + errorHelperText, + textFieldProps = {}, +}: Props) => { + return ( + ( + onChange(v || '')} + onBlur={onBlur} + value={value} + autoSelect + freeSolo + renderInput={params => ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + {...textFieldProps} + /> + )} + /> + )} + /> + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx new file mode 100644 index 0000000000..0bf7746e6a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx @@ -0,0 +1,113 @@ +/* + * 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 { FormHelperText, TextField } from '@material-ui/core'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { PreparePullRequestForm } from './PreparePullRequestForm'; + +describe('', () => { + it('renders without exploding', async () => { + const onSubmitFn = jest.fn(); + + const { getByRole } = render( + ( + <> + + Submit{' '} + > + )} + onSubmit={onSubmitFn} + />, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).toBeCalledTimes(1); + expect(onSubmitFn.mock.calls[0][0]).toMatchObject({ main: 'default' }); + }); + + it('should register a text field', async () => { + const onSubmitFn = jest.fn(); + + const { getByRole, getByLabelText } = render( + ( + <> + + Submit + > + )} + onSubmit={onSubmitFn} + />, + ); + + await act(async () => { + userEvent.clear(getByLabelText('Main Field')); + await userEvent.type(getByLabelText('Main Field'), 'My Text'); + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).toBeCalledTimes(1); + expect(onSubmitFn.mock.calls[0][0]).toMatchObject({ main: 'My Text' }); + }); + + it('registers required attribute', async () => { + const onSubmitFn = jest.fn(); + + const { queryByText, getByRole } = render( + ( + <> + + {errors.main && ( + + Error in required main field + + )} + Submit{' '} + > + )} + onSubmit={onSubmitFn} + />, + ); + + expect(queryByText('Error in required main field')).not.toBeInTheDocument(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).not.toBeCalled(); + expect(queryByText('Error in required main field')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx new file mode 100644 index 0000000000..9d59f4e10e --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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 { + SubmitHandler, + UnpackNestedValue, + useForm, + UseFormMethods, + UseFormOptions, +} from 'react-hook-form'; + +type Props> = Pick< + UseFormOptions, + 'defaultValues' +> & { + onSubmit: SubmitHandler; + + render: ( + props: Pick< + UseFormMethods, + 'errors' | 'register' | 'control' + > & { + values: UnpackNestedValue; + }, + ) => React.ReactNode; +}; + +/** + * A form wrapper that creates a form that is used to prepare a pull request. It + * hosts the form logic. + * + * @param defaultValues the default values of the form + * @param onSubmit a callback that is executed when the form is submitted + * (initiated by a button of type="submit") + * @param render render the form elements + */ +export const PreparePullRequestForm = < + TFieldValues extends Record +>({ + defaultValues, + onSubmit, + render, +}: Props) => { + const { handleSubmit, watch, control, register, errors } = useForm< + TFieldValues + >({ mode: 'onTouched', defaultValues }); + + return ( + + {render({ values: watch(), errors, register, control })} + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx new file mode 100644 index 0000000000..5bc22dbd42 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; + +const useStyles = makeStyles({ + displayNone: { + display: 'none', + }, +}); + +const entities: Entity[] = [ + { + apiVersion: '1', + kind: 'Kind', + metadata: { + name: 'name', + }, + }, + { + apiVersion: '1', + kind: 'Kind_2', + metadata: { + name: 'name', + }, + }, +]; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).toBeVisible(); + }); + + it('renders card with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).not.toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).not.toBeVisible(); + }); + + it('renders with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).not.toBeVisible(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx new file mode 100644 index 0000000000..c10c011820 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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 { CodeSnippet } from '@backstage/core'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; +import React from 'react'; +import YAML from 'yaml'; + +type Props = { + repositoryUrl: string; + entities: Entity[]; + classes?: { card?: string; cardContent?: string }; +}; + +export const PreviewCatalogInfoComponent = ({ + repositoryUrl, + entities, + classes, +}: Props) => { + return ( + + {`${repositoryUrl.replace( + /[\/]*$/, + '', + )}/catalog-info.yaml`} + } + /> + + + YAML.stringify(e)) + .join('---\n') + .trim()} + language="yaml" + /> + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx new file mode 100644 index 0000000000..1556bd9b41 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; + +const useStyles = makeStyles({ + displayNone: { + display: 'none', + }, +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).toBeVisible(); + }); + + it('renders card with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).not.toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).not.toBeVisible(); + }); + + it('renders with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).not.toBeVisible(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx new file mode 100644 index 0000000000..01af05d43a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 { MarkdownContent } from '@backstage/core'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; +import React from 'react'; + +type Props = { + title: string; + description: string; + classes?: { card?: string; cardContent?: string }; +}; + +export const PreviewPullRequestComponent = ({ + title, + description, + classes, +}: Props) => { + return ( + + + + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx new file mode 100644 index 0000000000..0b38fdaf58 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -0,0 +1,287 @@ +/* + * 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 { ApiProvider, ApiRegistry } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { TextField } from '@material-ui/core'; +import { act, render, RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; + +describe('', () => { + const catalogImportApi: jest.Mocked = { + analyzeUrl: jest.fn(), + submitPullRequest: jest.fn(), + }; + + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + addLocation: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const onPrepareFn = jest.fn(); + const analyzeResult = { + type: 'repository', + url: 'https://my-repository', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + spec: { + owner: 'my-owner', + }, + }, + ], + } as Extract; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); + + await act(async () => { + const { getByText } = render( + { + return ( + <> + + + + + > + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + const title = getByText('My title'); + const description = getByText('body', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).toBeVisible(); + }); + }); + + it('should submit created PR', async () => { + catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); + catalogImportApi.submitPullRequest.mockReturnValue( + Promise.resolve({ + location: 'https://my/location.yaml', + link: 'https://my/pull', + }), + ); + + let result: RenderResult; + await act(async () => { + result = await render( + { + return ( + <> + + + + + > + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + await userEvent.type(await result.getByLabelText('name'), '-changed'); + await userEvent.type(await result.getByLabelText('owner'), '-changed'); + await userEvent.click( + await result.getByRole('button', { name: /Create PR/i }), + ); + }); + + expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1); + expect(catalogImportApi.submitPullRequest.mock.calls[0]).toMatchObject([ + { + body: 'My **body**', + fileContent: `apiVersion: "1" +kind: Component +metadata: + name: my-component-changed + namespace: default +spec: + owner: my-owner-changed +`, + repositoryUrl: 'https://my-repository', + title: 'My title', + }, + ]); + expect(onPrepareFn).toBeCalledTimes(1); + expect(onPrepareFn.mock.calls[0]).toMatchObject([ + { + type: 'repository', + url: 'https://my-repository', + integrationType: 'github', + pullRequest: { + url: 'https://my/pull', + }, + locations: [ + { + entities: [ + { + kind: 'Component', + name: 'my-component-changed', + namespace: 'default', + }, + ], + target: 'https://my/location.yaml', + }, + ], + }, + { + notRepeatable: true, + }, + ]); + }); + + it('should show error message', async () => { + catalogApi.getEntities.mockResolvedValueOnce({ items: [] }); + catalogImportApi.submitPullRequest.mockRejectedValueOnce( + new Error('some error'), + ); + + let result: RenderResult; + await act(async () => { + result = await render( + { + return ( + <> + + + + + > + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + await userEvent.click( + await result.getByRole('button', { name: /Create PR/i }), + ); + }); + + expect(result!.getByText('some error')).toBeInTheDocument(); + expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1); + expect(onPrepareFn).toBeCalledTimes(0); + }); + + it('should load groups', async () => { + const renderFormFieldsFn = jest.fn(); + catalogApi.getEntities.mockReturnValue( + Promise.resolve({ + items: [ + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'my-group', + }, + }, + ], + }), + ); + + await act(async () => { + await render( + , + { + wrapper: Wrapper, + }, + ); + }); + + expect(catalogApi.getEntities).toBeCalledTimes(1); + expect(renderFormFieldsFn).toBeCalled(); + expect(renderFormFieldsFn.mock.calls[0][0]).toMatchObject({ + groups: [], + groupsLoading: true, + }); + expect( + renderFormFieldsFn.mock.calls[ + renderFormFieldsFn.mock.calls.length - 1 + ][0], + ).toMatchObject({ groups: ['Group:my-group'], groupsLoading: false }); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx new file mode 100644 index 0000000000..d9e7b962ab --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -0,0 +1,249 @@ +/* + * Copyright 2021 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, serializeEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Box, FormHelperText, Grid, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { useCallback, useState } from 'react'; +import { UseFormMethods } from 'react-hook-form'; +import { useAsync } from 'react-use'; +import YAML from 'yaml'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { PartialEntity } from '../../types'; +import { BackButton, NextButton } from '../Buttons'; +import { PrepareResult } from '../useImportState'; +import { PreparePullRequestForm } from './PreparePullRequestForm'; +import { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; + +const useStyles = makeStyles(theme => ({ + previewCard: { + marginTop: theme.spacing(1), + }, + previewCardContent: { + paddingTop: 0, + }, +})); + +type FormData = { + title: string; + body: string; + componentName: string; + owner: string; +}; + +type Props = { + analyzeResult: Extract; + onPrepare: ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, + ) => void; + onGoBack?: () => void; + + defaultTitle: string; + defaultBody: string; + + renderFormFields: ( + props: Pick, 'errors' | 'register' | 'control'> & { + groups: string[]; + groupsLoading: boolean; + }, + ) => React.ReactNode; +}; + +function generateEntities( + entities: PartialEntity[], + componentName: string, + owner: string, +): Entity[] { + return entities.map(e => ({ + ...e, + apiVersion: e.apiVersion!, + kind: e.kind!, + metadata: { + ...e.metadata, + name: componentName, + namespace: e.metadata?.namespace ?? 'default', + }, + spec: { + ...e.spec, + owner, + }, + })); +} + +export const StepPrepareCreatePullRequest = ({ + analyzeResult, + onPrepare, + onGoBack, + renderFormFields, + defaultTitle, + defaultBody, +}: Props) => { + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); + const catalogInfoApi = useApi(catalogImportApiRef); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(); + + const { loading: groupsLoading, value: groups } = useAsync(async () => { + const groupEntities = await catalogApi.getEntities({ + filter: { kind: 'group' }, + }); + + // TODO: defaultKind (=group), defaultNamespace (=same as entity) + return groupEntities.items.map(e => serializeEntityRef(e) as string).sort(); + }); + + const handleResult = useCallback( + async (data: FormData) => { + setSubmitted(true); + + try { + const pr = await catalogInfoApi.submitPullRequest({ + repositoryUrl: analyzeResult.url, + title: data.title, + body: data.body, + fileContent: generateEntities( + analyzeResult.generatedEntities, + data.componentName, + data.owner, + ) + .map(e => YAML.stringify(e)) + .join('---\n'), + }); + + onPrepare( + { + type: 'repository', + url: analyzeResult.url, + integrationType: analyzeResult.integrationType, + pullRequest: { + url: pr.link, + }, + locations: [ + { + target: pr.location, + entities: generateEntities( + analyzeResult.generatedEntities, + data.componentName, + data.owner, + ).map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace!, + name: e.metadata.name, + })), + }, + ], + }, + { notRepeatable: true }, + ); + } catch (e) { + setError(e.message); + setSubmitted(false); + } + }, + [ + analyzeResult.generatedEntities, + analyzeResult.integrationType, + analyzeResult.url, + catalogInfoApi, + onPrepare, + ], + ); + + return ( + <> + + You entered a link to a {analyzeResult.integrationType} repository but + we didn't found a catalog-info.yaml. Use this form to + create a Pull Request that creates an initial{' '} + catalog-info.yaml. + + + + onSubmit={handleResult} + defaultValues={{ + title: defaultTitle, + body: defaultBody, + owner: + (analyzeResult.generatedEntities[0]?.spec?.owner as string) || '', + componentName: + analyzeResult.generatedEntities[0]?.metadata?.name || '', + }} + render={({ values, errors, control, register }) => ( + <> + {renderFormFields({ + errors, + register, + control, + groups: groups ?? [], + groupsLoading, + })} + + + Preview Pull Request + + + + + + Preview Entities + + + + + {error && {error}} + + + {onGoBack && ( + + )} + + Create PR + + + > + )} + /> + > + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts new file mode 100644 index 0000000000..119feee4a1 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 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 { AutocompleteTextField } from './AutocompleteTextField'; +export { PreparePullRequestForm } from './PreparePullRequestForm'; +export { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +export { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; +export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx new file mode 100644 index 0000000000..82da9b9572 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx @@ -0,0 +1,202 @@ +/* + * 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 { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult } from '../../api'; +import { StepPrepareSelectLocations } from './StepPrepareSelectLocations'; + +describe('', () => { + const analyzeResult = { + type: 'locations', + locations: [ + { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }, + { + target: 'url-2', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + { + kind: 'api', + namespace: 'default', + name: 'name', + }, + ], + }, + ], + } as Extract; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + const { getByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + }); + + it('should select and deselect all', async () => { + const { getByRole, getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + checkboxes.forEach(c => expect(c).not.toBeChecked()); + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Select All/i })); + }); + + checkboxes.forEach(c => expect(c).toBeChecked()); + expect(getByRole('button', { name: /Review/i })).not.toBeDisabled(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Select All/i })); + }); + + checkboxes.forEach(c => expect(c).not.toBeChecked()); + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + }); + + it('should preselect prepared locations', async () => { + const { getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + }); + + it('should select items', async () => { + const { getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + checkboxes.forEach(c => expect(c).not.toBeChecked()); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + checkboxes.forEach(c => expect(c).not.toBeChecked()); + }); + + it('should go back', async () => { + const onGoBack = jest.fn(); + + const { getByRole } = render( + undefined} + onGoBack={onGoBack} + />, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Back/i })); + }); + + expect(onGoBack).toBeCalledTimes(1); + }); + + it('should submit', async () => { + const onPrepare = jest.fn(); + + const { getAllByRole, getByRole } = render( + undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Review/i })); + }); + + expect(onPrepare).toBeCalledTimes(1); + expect(onPrepare.mock.calls[0][0]).toMatchObject({ + type: 'locations', + locations: [ + { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }, + ], + } as Extract); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx new file mode 100644 index 0000000000..d44472fb5a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx @@ -0,0 +1,124 @@ +/* + * Copyright 2021 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 { + Checkbox, + Grid, + ListItem, + ListItemIcon, + ListItemText, + Typography, +} from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; +import { AnalyzeResult } from '../../api'; +import { BackButton, NextButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { PrepareResult } from '../useImportState'; + +type Props = { + analyzeResult: Extract; + prepareResult?: PrepareResult; + onPrepare: (result: PrepareResult) => void; + onGoBack?: () => void; +}; + +/** + * A form that lets a user select one of a list of locations to import + * + * @param analyzeResult the result of the analysis + * @param prepareResult the selectected locations from a previous step + * @param onPrepare called after the selection + * @param onGoBack called to go back to the previous step + */ +export const StepPrepareSelectLocations = ({ + analyzeResult, + prepareResult, + onPrepare, + onGoBack, +}: Props) => { + const [selectedUrls, setSelectedUrls] = useState( + prepareResult?.locations.map(l => l.target) || [], + ); + + const handleResult = useCallback(async () => { + onPrepare({ + type: 'locations', + locations: analyzeResult.locations.filter((l: any) => + selectedUrls.includes(l.target), + ), + }); + }, [analyzeResult.locations, onPrepare, selectedUrls]); + + const onItemClick = (url: string) => { + setSelectedUrls(urls => + urls.includes(url) ? urls.filter(u => u !== url) : urls.concat(url), + ); + }; + + const onSelectAll = () => { + setSelectedUrls(urls => + urls.length < analyzeResult.locations.length + ? analyzeResult.locations.map(l => l.target) + : [], + ); + }; + + return ( + <> + + Select one or more locations that are present in your git repository: + + + + + 0 && + selectedUrls.length < analyzeResult.locations.length + } + tabIndex={-1} + disableRipple + /> + + + + } + onItemClick={onItemClick} + locations={analyzeResult.locations} + locationListItemIcon={target => ( + + )} + collapsed + /> + + + {onGoBack && } + + Review + + + > + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts b/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts new file mode 100644 index 0000000000..db98a63be0 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { StepPrepareSelectLocations } from './StepPrepareSelectLocations'; diff --git a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx new file mode 100644 index 0000000000..eb2268f212 --- /dev/null +++ b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2021 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 { configApiRef, Link, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { FormHelperText, Grid, Typography } from '@material-ui/core'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React, { useCallback, useState } from 'react'; +import { BackButton, NextButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { PrepareResult, ReviewResult } from '../useImportState'; + +type Props = { + prepareResult: PrepareResult; + onReview: (result: ReviewResult) => void; + onGoBack?: () => void; +}; + +export const StepReviewLocation = ({ + prepareResult, + onReview, + onGoBack, +}: Props) => { + const catalogApi = useApi(catalogApiRef); + const configApi = useApi(configApiRef); + + const appTitle = configApi.getOptional('app.title') || 'Backstage'; + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(); + + const handleImport = useCallback(async () => { + setSubmitted(true); + try { + const result = await Promise.all( + prepareResult.locations.map(l => + catalogApi.addLocation({ + type: 'url', + target: l.target, + presence: + prepareResult.type === 'repository' ? 'optional' : 'required', + }), + ), + ); + + onReview({ + ...prepareResult, + locations: result.map(r => ({ + target: r.location.target, + entities: r.entities, + })), + }); + } catch (e) { + // TODO: this error should be handled differently. We add it as 'optional' and + // it is not uncommon that a PR has not been merged yet. + if ( + prepareResult.type === 'repository' && + e.message.startsWith( + 'Location was added but has no entities specified yet', + ) + ) { + onReview({ + ...prepareResult, + locations: prepareResult.locations.map(l => ({ + target: l.target, + entities: [], + })), + }); + } else { + setError(e.message); + setSubmitted(false); + } + } + }, [prepareResult, onReview, catalogApi]); + + return ( + <> + {prepareResult.type === 'repository' && ( + <> + + The following Pull Request has been opened:{' '} + + {prepareResult.pullRequest.url} + + + + + You can already import the location and {appTitle} will fetch the + entities as soon as the Pull Request is merged. + + > + )} + + + The following entities will be added to the catalog: + + + } + /> + + {error && {error}} + + + {onGoBack && } + handleImport()} + > + Import + + + > + ); +}; diff --git a/plugins/catalog-import/src/components/StepReviewLocation/index.ts b/plugins/catalog-import/src/components/StepReviewLocation/index.ts new file mode 100644 index 0000000000..2c93e98437 --- /dev/null +++ b/plugins/catalog-import/src/components/StepReviewLocation/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { StepReviewLocation } from './StepReviewLocation'; diff --git a/plugins/catalog-import/src/components/index.ts b/plugins/catalog-import/src/components/index.ts new file mode 100644 index 0000000000..180752c873 --- /dev/null +++ b/plugins/catalog-import/src/components/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 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 './ImportStepper'; +export * from './EntityListComponent'; +export * from './StepInitAnalyzeUrl'; +export * from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/useImportState.test.tsx b/plugins/catalog-import/src/components/useImportState.test.tsx new file mode 100644 index 0000000000..d1ef3184ad --- /dev/null +++ b/plugins/catalog-import/src/components/useImportState.test.tsx @@ -0,0 +1,372 @@ +/* + * 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, EntityName } from '@backstage/catalog-model'; +import { cleanup } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { AnalyzeResult } from '../api'; + +import { + ImportState, + PrepareResult, + ReviewResult, + useImportState, +} from './useImportState'; + +describe('useImportState', () => { + const as = ( + curr: ImportState, + _: T, + ) => curr as Extract; + + const locationAP: AnalyzeResult & PrepareResult = { + type: 'locations', + locations: [ + { + target: 'https://0', + entities: [] as EntityName[], + }, + ], + }; + + const locationR: ReviewResult = { + type: 'locations', + locations: [ + { + target: 'https://0', + entities: [] as Entity[], + }, + ], + }; + + it('should use initial url', async () => { + const { result } = renderHook(() => + useImportState({ initialUrl: 'http://my-url' }), + ); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: 'http://my-url', + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + }); + + describe('onAnalysis & onPrepare & onReview & onReset', () => { + it('should work', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 1, + analysisUrl: 'http://my-url', + activeState: 'prepare', + analyzeResult: locationAP, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'prepare').onPrepare(locationAP); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 2, + analysisUrl: 'http://my-url', + activeState: 'review', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 3, + analysisUrl: 'http://my-url', + activeState: 'finish', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: locationR, + }); + + act(() => result.current.onReset()); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: locationR, + }); + }); + + it('should work skipped', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + { + prepareResult: locationAP, + }, + ); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 2, + analysisUrl: 'http://my-url', + activeState: 'review', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 3, + analysisUrl: 'http://my-url', + activeState: 'finish', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: locationR, + }); + }); + + it('should ignore on invalid state', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + // state 'analyze' + act(() => { + as(result.current, 'prepare').onPrepare(locationAP); + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current.activeState).toBe('analyze'); + expect(result.current.activeFlow).toBe('unknown'); + + // switch state to 'prepare' + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + + // state 'prepare' + act(() => { + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ); + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current.activeState).toBe('prepare'); + expect(result.current.activeFlow).toBe('single-location'); + + // switch to 'review' + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + + // state 'review' + act(() => { + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ); + as(result.current, 'prepare').onPrepare({ + type: 'locations', + locations: [], + }); + }); + + expect(result.current.activeState).toBe('review'); + expect(result.current.activeFlow).toBe('single-location'); + expect( + as(result.current, 'prepare').prepareResult!.locations, + ).not.toEqual([]); + + // switch to 'finish' + act(() => as(result.current, 'review').onReview(locationR)); + + expect(result.current.activeState).toBe('finish'); + expect(result.current.activeFlow).toBe('single-location'); + }); + }); + + describe('onGoBack', () => { + it('should work', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + expect(result.current.activeStepNumber).toBe(1); + + expect(result.current.onGoBack).not.toBeUndefined(); + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(0); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + expect(result.current.activeStepNumber).toBe(2); + + expect(result.current.onGoBack).not.toBeUndefined(); + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(1); + + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + act(() => as(result.current, 'review').onReview(locationR)); + expect(result.current.activeStepNumber).toBe(3); + }); + + it('should work for skipped', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + { + prepareResult: locationAP, + }, + ), + ); + expect(result.current.activeStepNumber).toBe(2); + expect(result.current.onGoBack).not.toBeUndefined(); + + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + }); + + describe('should consider prepareNotRepeatable', () => { + it('as true', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ), + ); + act(() => + as(result.current, 'prepare').onPrepare(locationAP, { + notRepeatable: true, + }), + ); + + expect(result.current.onGoBack).toBeUndefined(); + }); + + it('as false', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ), + ); + act(() => + as(result.current, 'prepare').onPrepare(locationAP, { + notRepeatable: false, + }), + ); + + expect(result.current.onGoBack).not.toBeUndefined(); + }); + }); + }); +}); diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts new file mode 100644 index 0000000000..f042c991ef --- /dev/null +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -0,0 +1,291 @@ +/* + * Copyright 2021 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, EntityName } from '@backstage/catalog-model'; +import { useReducer } from 'react'; +import { AnalyzeResult } from '../api'; + +// the configuration of the stepper +export type ImportFlows = + | 'unknown' + | 'single-location' + | 'multiple-locations' + | 'no-location'; + +// the available states of the stepper +type ImportStateTypes = 'analyze' | 'prepare' | 'review' | 'finish'; + +// result of the prepare state +export type PrepareResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + }; + +// result of the review result +export type ReviewResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: Entity[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: Entity[]; + }>; + }; + +// function type for the 'analysis' -> 'prepare'/'review' transition +type onAnalysisFn = ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, +) => void; + +// function type for the 'prepare' -> 'review' transition +type onPrepareFn = ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, +) => void; + +// function type for the 'review' -> 'finish' transition +type onReviewFn = (result: ReviewResult) => void; + +// the type interfaces that are available in each state. every state provides +// already known information and means to go to the next, or the previous step. +type State = + | { + activeState: 'analyze'; + onAnalysis: onAnalysisFn; + } + | { + activeState: 'prepare'; + analyzeResult: AnalyzeResult; + prepareResult?: PrepareResult; + onPrepare: onPrepareFn; + } + | { + activeState: 'review'; + analyzeResult: AnalyzeResult; + prepareResult: PrepareResult; + onReview: onReviewFn; + } + | { + activeState: 'finish'; + analyzeResult: AnalyzeResult; + prepareResult: PrepareResult; + reviewResult: ReviewResult; + }; + +export type ImportState = State & { + activeFlow: ImportFlows; + activeStepNumber: number; + analysisUrl?: string; + + onGoBack?: () => void; + onReset: () => void; +}; + +type ReducerActions = + | { type: 'onAnalysis'; args: Parameters } + | { type: 'onPrepare'; args: Parameters } + | { type: 'onReview'; args: Parameters } + | { type: 'onGoBack' } + | { type: 'onReset'; initialUrl?: string }; + +type ReducerState = { + activeFlow: ImportFlows; + activeState: ImportStateTypes; + analysisUrl?: string; + analyzeResult?: AnalyzeResult; + prepareResult?: PrepareResult; + reviewResult?: ReviewResult; + + previousStates: ImportStateTypes[]; +}; + +function init(initialUrl?: string): ReducerState { + return { + activeFlow: 'unknown', + activeState: 'analyze', + analysisUrl: initialUrl, + previousStates: [], + }; +} + +function reducer(state: ReducerState, action: ReducerActions): ReducerState { + switch (action.type) { + case 'onAnalysis': { + if (state.activeState !== 'analyze') { + return state; + } + + const { activeState, previousStates } = state; + const [activeFlow, analysisUrl, analyzeResult, opts] = action.args; + + return { + ...state, + analysisUrl, + activeFlow, + analyzeResult, + prepareResult: opts?.prepareResult, + + activeState: opts?.prepareResult === undefined ? 'prepare' : 'review', + previousStates: previousStates.concat(activeState), + }; + } + + case 'onPrepare': { + if (state.activeState !== 'prepare') { + return state; + } + + const { activeState, previousStates } = state; + const [prepareResult, opts] = action.args; + + return { + ...state, + prepareResult, + + activeState: 'review', + previousStates: opts?.notRepeatable + ? [] + : previousStates.concat(activeState), + }; + } + + case 'onReview': { + if (state.activeState !== 'review') { + return state; + } + + const { activeState, previousStates } = state; + const [reviewResult] = action.args; + + return { + ...state, + reviewResult, + + activeState: 'finish', + previousStates: previousStates.concat(activeState), + }; + } + + case 'onGoBack': { + const { activeState, previousStates } = state; + + return { + ...state, + + activeState: + previousStates.length > 0 + ? previousStates[previousStates.length - 1] + : activeState, + previousStates: previousStates.slice(0, previousStates.length - 1), + }; + } + + case 'onReset': + return { + ...init(action.initialUrl), + + // we keep the old reviewResult since the form is animated and an + // undefined value might crash the last step. + reviewResult: state.reviewResult, + }; + + default: + throw new Error(); + } +} + +/** + * A hook that manages the state machine of the form. It handles different flows + * which each can implement up to four states: + * 1. analyze + * 2. preview (skippable from analyze->review) + * 3. review + * 4. finish + * + * @param options options + */ +export const useImportState = (options?: { + initialUrl?: string; +}): ImportState => { + const [state, dispatch] = useReducer(reducer, options?.initialUrl, init); + + const { activeFlow, activeState, analysisUrl, previousStates } = state; + + return { + activeFlow, + activeState, + activeStepNumber: ['analyze', 'prepare', 'review', 'finish'].indexOf( + activeState, + ), + analysisUrl: analysisUrl, + + analyzeResult: state.analyzeResult!, + prepareResult: state.prepareResult!, + reviewResult: state.reviewResult!, + + onAnalysis: (flow, url, result, opts) => + dispatch({ + type: 'onAnalysis', + args: [flow, url, result, opts], + }), + + onPrepare: (result, opts) => + dispatch({ + type: 'onPrepare', + args: [result, opts], + }), + + onReview: result => dispatch({ type: 'onReview', args: [result] }), + + onGoBack: + previousStates.length > 0 + ? () => dispatch({ type: 'onGoBack' }) + : undefined, + + onReset: () => + dispatch({ type: 'onReset', initialUrl: options?.initialUrl }), + }; +}; diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts index 7b4fc17121..562e0499f3 100644 --- a/plugins/catalog-import/src/index.ts +++ b/plugins/catalog-import/src/index.ts @@ -20,4 +20,5 @@ export { CatalogImportPage, } from './plugin'; export { Router } from './components/Router'; +export * from './components'; export * from './api'; diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json deleted file mode 100644 index eaadf1054a..0000000000 --- a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "existingEntityFiles": [], - "generateEntities": [ - { - "entity": { - "apiVersion": "backstage.io/v1alpha1", - "kind": "Component", - "metadata": { - "name": "somerepo", - "annotations": { - "github.com/project-slug": "someuser/somerepo" - } - }, - "spec": { - "type": "other", - "lifecycle": "unknown" - } - }, - "fields": [] - } - ] -} diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json deleted file mode 100644 index c44a0f4d61..0000000000 --- a/plugins/catalog-import/src/mocks/locations-POST-response.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "location": { - "id": "d4a64359-a709-4c91-a9de-0905a033bf22", - "type": "url", - "target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml" - }, - "entities": [ - { - "metadata": { - "namespace": "default", - "annotations": { - "backstage.io/managed-by-location": "url:https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml", - "github.com/project-slug": "someusername/somerepo" - }, - "name": "somerepo", - "uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146", - "etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0", - "generation": 1 - }, - "apiVersion": "backstage.io/v1alpha1", - "kind": "Component", - "spec": { - "type": "other", - "lifecycle": "unknown", - "owner": "unknown" - }, - "relations": [ - { - "target": { - "kind": "group", - "namespace": "default", - "name": "unknown" - }, - "type": "ownedBy" - } - ] - } - ] -} diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index 42210d0ed1..18b3124b3b 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -15,16 +15,17 @@ */ import { + identityApiRef, + configApiRef, createApiFactory, createPlugin, + createRoutableExtension, createRouteRef, discoveryApiRef, githubAuthApiRef, - configApiRef, - createRoutableExtension, } from '@backstage/core'; -import { catalogImportApiRef } from './api/CatalogImportApi'; -import { CatalogImportClient } from './api/CatalogImportClient'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogImportApiRef, CatalogImportClient } from './api'; export const rootRouteRef = createRouteRef({ path: '', @@ -39,10 +40,24 @@ export const catalogImportPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, githubAuthApi: githubAuthApiRef, + identityApi: identityApiRef, configApi: configApiRef, + catalogApi: catalogApiRef, }, - factory: ({ discoveryApi, githubAuthApi, configApi }) => - new CatalogImportClient({ discoveryApi, githubAuthApi, configApi }), + factory: ({ + discoveryApi, + githubAuthApi, + identityApi, + configApi, + catalogApi, + }) => + new CatalogImportClient({ + discoveryApi, + githubAuthApi, + configApi, + identityApi, + catalogApi, + }), }), ], routes: { diff --git a/plugins/catalog-import/src/util/types.ts b/plugins/catalog-import/src/types.ts similarity index 100% rename from plugins/catalog-import/src/util/types.ts rename to plugins/catalog-import/src/types.ts diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts deleted file mode 100644 index a2d299b9f4..0000000000 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ /dev/null @@ -1,121 +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 * as YAML from 'yaml'; -import { useApi, configApiRef } from '@backstage/core'; -import { catalogImportApiRef } from '../api/CatalogImportApi'; -import { ConfigSpec } from '../components/ImportComponentPage'; -import parseGitUrl from 'git-url-parse'; - -// TODO: (O5ten) Refactor into a core API instead of direct usage like this -// https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430 -import { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, -} from '@backstage/integration'; - -export function useGithubRepos() { - const api = useApi(catalogImportApiRef); - const config = useApi(configApiRef); - - const getGithubIntegrationConfig = (location: string) => { - const { - name: repoName, - owner: ownerName, - resource: hostname, - } = parseGitUrl(location); - - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - const githubIntegrationConfig = configs.find(v => v.host === hostname); - if (!githubIntegrationConfig) { - throw new Error( - `Unable to locate github-integration for repo-location, ${location}`, - ); - } - return { - repoName, - ownerName, - githubIntegrationConfig, - }; - }; - - const submitPrToRepo = async (selectedRepo: ConfigSpec) => { - const { - repoName, - ownerName, - githubIntegrationConfig, - } = getGithubIntegrationConfig(selectedRepo.location); - const submitPRResponse = await api - .submitPrToRepo({ - owner: ownerName, - repo: repoName, - fileContent: selectedRepo.config - .map(entity => `---\n${YAML.stringify(entity)}`) - .join('\n'), - githubIntegrationConfig, - }) - .catch(e => { - throw new Error(`Failed to submit PR to repo, ${e.message}`); - }); - - await api - .createRepositoryLocation({ - location: submitPRResponse.location, - }) - .catch(e => { - throw new Error(`Failed to create repository location, ${e.message}`); - }); - - return submitPRResponse; - }; - - const checkForExistingCatalogInfo = async ( - location: string, - ): Promise<{ exists: boolean; url?: string }> => { - let githubConfig: { - repoName: string; - ownerName: string; - githubIntegrationConfig: GitHubIntegrationConfig; - }; - try { - githubConfig = getGithubIntegrationConfig(location); - } catch (e) { - return { exists: false }; - } - return await api - .checkForExistingCatalogInfo({ - owner: githubConfig.ownerName, - repo: githubConfig.repoName, - githubIntegrationConfig: githubConfig.githubIntegrationConfig, - }) - .catch(e => { - throw new Error( - `Failed to inspect repository for existing catalog-info.yaml, ${e.message}`, - ); - }); - }; - - return { - submitPrToRepo, - checkForExistingCatalogInfo, - generateEntityDefinitions: (repo: string) => - api.generateEntityDefinitions({ repo }), - addLocation: (location: string) => - api.createRepositoryLocation({ location }), - }; -} diff --git a/plugins/catalog-import/src/util/validate.test.ts b/plugins/catalog-import/src/util/validate.test.ts deleted file mode 100644 index 139c7fb0f8..0000000000 --- a/plugins/catalog-import/src/util/validate.test.ts +++ /dev/null @@ -1,33 +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 { ComponentIdValidators } from './validate'; - -describe('ComponentIdValidators', () => { - describe('httpsValidator validator', () => { - const errorMessage = 'Must start with https://.'; - test.each([ - [true, 'https://example.com'], - [errorMessage, 'http://example.com'], - [errorMessage, 'example.com'], - [errorMessage, 'www.example.com'], - [errorMessage, ''], - [errorMessage, undefined], - ])('should return %p for %s', (expected: string | boolean, arg: any) => { - expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected); - }); - }); -}); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 90b7c80caf..38cc17e1ba 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-react +## 0.0.3 + +### Patch Changes + +- 19d354c78: Make `EntityRefLink` a `React.forwardRef` in order to use it as root component in other components like `ListItem`. +- Updated dependencies [6ed2b47d6] +- Updated dependencies [72b96e880] +- Updated dependencies [b51ee6ece] + - @backstage/catalog-client@0.3.6 + - @backstage/core@0.6.1 + ## 0.0.2 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 64307f6846..649f3b2ff6 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.0.2", + "version": "0.0.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,9 +28,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.5", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@material-ui/core": "^4.11.0", "@types/react": "^16.9", "react": "^16.13.1", @@ -40,8 +40,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index ae6f807275..50a80af12a 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -15,13 +15,12 @@ */ import { Entity, - EntityName, ENTITY_DEFAULT_NAMESPACE, + EntityName, } from '@backstage/catalog-model'; -import { Link } from '@material-ui/core'; +import { Link } from '@backstage/core'; import React from 'react'; import { generatePath } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; import { entityRoute } from '../../routes'; import { formatEntityRefTitle } from './format'; @@ -31,41 +30,42 @@ type EntityRefLinkProps = { children?: React.ReactNode; }; -export const EntityRefLink = ({ - entityRef, - defaultKind, - children, -}: EntityRefLinkProps) => { - let kind; - let namespace; - let name; +export const EntityRefLink = React.forwardRef( + (props, ref) => { + const { entityRef, defaultKind, children, ...linkProps } = props; - if ('metadata' in entityRef) { - kind = entityRef.kind; - namespace = entityRef.metadata.namespace; - name = entityRef.metadata.name; - } else { - kind = entityRef.kind; - namespace = entityRef.namespace; - name = entityRef.name; - } + let kind; + let namespace; + let name; - kind = kind.toLowerCase(); + if ('metadata' in entityRef) { + kind = entityRef.kind; + namespace = entityRef.metadata.namespace; + name = entityRef.metadata.name; + } else { + kind = entityRef.kind; + namespace = entityRef.namespace; + name = entityRef.name; + } - const routeParams = { - kind, - namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, - name, - }; + kind = kind.toLowerCase(); - // TODO: Use useRouteRef here to generate the path - return ( - - {children} - {!children && formatEntityRefTitle(entityRef, { defaultKind })} - - ); -}; + const routeParams = { + kind, + namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, + name, + }; + + // TODO: Use useRouteRef here to generate the path + return ( + + {children} + {!children && formatEntityRefTitle(entityRef, { defaultKind })} + + ); + }, +); diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index eb4c9a30d7..d786fc2f15 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-catalog +## 0.3.1 + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- ca559171b: bug fix: 3310 fixes reloading entities with the default owned filter +- f5e564cd6: Improve display of error messages +- 1df75733e: Adds an `EntityLinksCard` component to display `entity.metadata.links` on entity pages. The new component is a companion for the new [Entity Links](https://backstage.io/docs/features/software-catalog/descriptor-format#links-optional) catalog model addition. + + Here is an example usage within an `EntityPage.tsx`. + + ```tsx + // in packages/app/src/components/catalog/EntityPage.tsx + const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( + + + + // or ... + + + + ); + ``` + +- e5da858d7: Removed unused functions and the moment library. #4278 +- 9230d07e7: Fix whitespace around variable in unregister error dialog box +- Updated dependencies [6ed2b47d6] +- Updated dependencies [72b96e880] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/catalog-client@0.3.6 + - @backstage/plugin-scaffolder@0.5.0 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.0 ### Minor Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index c806c58e67..072acc6472 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.5", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/plugin-scaffolder": "^0.4.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/plugin-scaffolder": "^0.5.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,7 +42,6 @@ "@types/react": "^16.9", "classnames": "^2.2.6", "git-url-parse": "^11.4.4", - "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-helmet": "6.1.0", @@ -53,8 +52,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@microsoft/microsoft-graph-types": "^1.25.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts new file mode 100644 index 0000000000..87dcd8f74b --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { CatalogClient } from '@backstage/catalog-client'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; + +jest.mock('@backstage/catalog-client'); +const MockedCatalogClient = CatalogClient as jest.Mock; + +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi: DiscoveryApi = { + async getBaseUrl(_pluginId) { + return mockBaseUrl; + }, +}; +const identityApi: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; +const guestIdentityApi: IdentityApi = { + getUserId() { + return 'guest'; + }, + getProfile() { + return {}; + }, + async getIdToken() { + return Promise.resolve(undefined); + }, + async signOut() { + return Promise.resolve(); + }, +}; + +describe('CatalogClientWrapper', () => { + beforeEach(() => { + MockedCatalogClient.mockClear(); + }); + + describe('getEntities', () => { + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.getEntities(); + const getEntities = MockedCatalogClient.mock.instances[0].getEntities; + expect(getEntities).toHaveBeenCalledWith(undefined, { + token: 'fake-id-token', + }); + expect(getEntities).toHaveBeenCalledTimes(1); + }); + }); + + describe('getLocationById', () => { + it('omits authorization token when guest', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi: guestIdentityApi, + }); + await client.getLocationById('42'); + const getLocationById = + MockedCatalogClient.mock.instances[0].getLocationById; + expect(getLocationById).toHaveBeenCalledWith('42', {}); + expect(getLocationById).toHaveBeenCalledTimes(1); + }); + }); + + describe('getEntityByName', () => { + const name = { + kind: 'kind', + namespace: 'namespace', + name: 'name', + }; + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.getEntityByName(name); + const getEntityByName = + MockedCatalogClient.mock.instances[0].getEntityByName; + expect(getEntityByName).toHaveBeenCalledWith(name, { + token: 'fake-id-token', + }); + expect(getEntityByName).toHaveBeenCalledTimes(1); + }); + }); + + describe('addLocation', () => { + const location = { target: 'target' }; + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.addLocation(location); + const addLocation = MockedCatalogClient.mock.instances[0].addLocation; + expect(addLocation).toHaveBeenCalledWith(location, { + token: 'fake-id-token', + }); + expect(addLocation).toHaveBeenCalledTimes(1); + }); + }); + + describe('getLocationByEntity', () => { + const entity = { + apiVersion: 'apiVersion', + kind: 'kind', + metadata: { + name: 'name', + }, + }; + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.getLocationByEntity(entity); + const getLocationByEntity = + MockedCatalogClient.mock.instances[0].getLocationByEntity; + expect(getLocationByEntity).toHaveBeenCalledWith(entity, { + token: 'fake-id-token', + }); + expect(getLocationByEntity).toHaveBeenCalledTimes(1); + }); + }); + + describe('removeEntityByUid', () => { + it('injects authorization token', async () => { + const uid = 'uid'; + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.removeEntityByUid(uid); + const removeEntityByUid = + MockedCatalogClient.mock.instances[0].removeEntityByUid; + expect(removeEntityByUid).toHaveBeenCalledWith(uid, { + token: 'fake-id-token', + }); + expect(removeEntityByUid).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts new file mode 100644 index 0000000000..42e9d872d4 --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -0,0 +1,97 @@ +/* + * 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, EntityName, Location } from '@backstage/catalog-model'; +import { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, + CatalogClient, +} from '@backstage/catalog-client'; +import { IdentityApi } from '@backstage/core'; + +type CatalogRequestOptions = { + token?: string; +}; + +/** + * CatalogClient wrapper that injects identity token for all requests + */ +export class CatalogClientWrapper implements CatalogApi { + private readonly identityApi: IdentityApi; + private readonly client: CatalogClient; + + constructor(options: { client: CatalogClient; identityApi: IdentityApi }) { + this.client = options.client; + this.identityApi = options.identityApi; + } + + async getLocationById( + id: String, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getLocationById(id, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async getEntities( + request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise> { + return await this.client.getEntities(request, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async getEntityByName( + compoundName: EntityName, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getEntityByName(compoundName, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async addLocation( + request: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.addLocation(request, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getLocationByEntity(entity, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.removeEntityByUid(uid, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } +} diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 90a1e15ad0..6de4c318b5 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -80,6 +80,14 @@ type Props = { onChange?: OnChangeCallback; }; +/** + * Sidebar filter type and human readable label for it. owned/starred/all + */ +export type CatalogFilterType = { + id: string; + label: string; +}; + /** * The main filter group in the sidebar, toggling owned/starred/all. */ diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 42c9cf1117..6d4d18e4f2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -30,7 +30,7 @@ import { } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { EntityFilterGroupsProvider } from '../../filter'; import { CatalogPage } from './CatalogPage'; @@ -128,4 +128,24 @@ describe('CatalogPage', () => { fireEvent.click(getByText(/All/)); expect(await findByText(/All \(2\)/)).toBeInTheDocument(); }); + // this test is for fixing the bug after favoriting an entity, the matching entities defaulting + // to "owned" filter and not based on the selected filter + it('should render the correct entities filtered on the selectedfilter', async () => { + const { findByText, findAllByTitle, getByText } = renderWrapped( + , + ); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByText(/Starred/)).toBeInTheDocument(); + fireEvent.click(getByText(/Starred/)); + expect(await findByText(/Starred \(0\)/)).toBeInTheDocument(); + fireEvent.click(getByText(/All/)); + expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + + const starredIcons = await findAllByTitle('Add to favorites'); + fireEvent.click(starredIcons[0]); + expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + + fireEvent.click(getByText(/Starred/)); + waitFor(() => expect(findByText(/Starred \(1\)/)).toBeInTheDocument()); + }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 1298d5e190..a2ebc63825 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -31,7 +31,11 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntities'; -import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; +import { + ButtonGroup, + CatalogFilter, + CatalogFilterType, +} from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { useOwnUser } from '../useOwnUser'; @@ -65,7 +69,9 @@ const CatalogPageContents = () => { const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); const [selectedTab, setSelectedTab] = useState(); - const [selectedSidebarItem, setSelectedSidebarItem] = useState(); + const [selectedSidebarItem, setSelectedSidebarItem] = useState< + CatalogFilterType + >(); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; const addMockData = useCallback(async () => { @@ -180,13 +186,15 @@ const CatalogPageContents = () => { setSelectedSidebarItem(label)} - initiallySelected="owned" + onChange={({ label, id }) => + setSelectedSidebarItem({ label, id }) + } + initiallySelected={selectedSidebarItem?.id ?? 'owned'} /> { - const valid = isValidDateAndFormat( - '1970-01-01T00:00:00', - 'YYYY-MM-DD[T]HH:mm:ss', - ); - const invalid = isValidDateAndFormat( - '1970/01/01T00:00:00', - 'YYYY-MM-DD[T]HH:mm:ss', - ); - expect(valid).toBe(true); - expect(invalid).toBe(false); -}); +import { getTimeBasedGreeting } from './timeUtil'; it('has greeting and language', () => { const greeting = getTimeBasedGreeting(); diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx new file mode 100644 index 0000000000..d54eac788c --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx @@ -0,0 +1,81 @@ +/* + * 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, EntityLink } from '@backstage/catalog-model'; +import { EntityContext } from '@backstage/plugin-catalog-react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { EntityLinksCard } from './EntityLinksCard'; + +describe('EntityLinksCard', () => { + const createEntity = (links: EntityLink[] = []): Entity => + ({ + metadata: { + links, + }, + } as Entity); + + const createLink = ({ + url = 'https://dashboard.dashexample.com', + title = 'admin dashboard', + icon = undefined, + }: Partial = {}): EntityLink => ({ + url, + title, + icon, + }); + + it('should render a link', async () => { + const links: EntityLink[] = [createLink()]; + const entityContextValue = { + entity: createEntity(links), + loading: false, + error: undefined, + }; + + const { queryByText } = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(queryByText('admin dashboard')).toBeInTheDocument(); + expect(queryByText('derp')).not.toBeInTheDocument(); + }); + + it('should show empty state', async () => { + const entityContextValue = { + entity: createEntity([]), + loading: false, + error: undefined, + }; + + const { queryByText } = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect( + queryByText(/.*No links defined for this entity.*/), + ).toBeInTheDocument(); + expect(queryByText('admin dashboard')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx new file mode 100644 index 0000000000..60fb9c3cf5 --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -0,0 +1,59 @@ +/* + * 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 { IconComponent, InfoCard, useApp } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import LanguageIcon from '@material-ui/icons/Language'; +import React from 'react'; +import { EntityLinksEmptyState } from './EntityLinksEmptyState'; +import { LinksGridList } from './LinksGridList'; +import { ColumnBreakpoints } from './types'; + +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; + cols?: ColumnBreakpoints | number; +}; + +export const EntityLinksCard = ({ cols = undefined }: Props) => { + const { entity } = useEntity(); + const app = useApp(); + + // TODO: Refactor App.icons & App.getSystemIcon to support custom icons + const iconResolver = (key: string | undefined): IconComponent => { + return app.getSystemIcon(key as any) ?? LanguageIcon; + }; + + const links = entity?.metadata?.links; + + return ( + + {!links || links.length === 0 ? ( + + ) : ( + ({ + text: title ?? url, + href: url, + Icon: iconResolver(icon), + }))} + /> + )} + + ); +}; diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx new file mode 100644 index 0000000000..3ced50decc --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -0,0 +1,65 @@ +/* + * 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 { CodeSnippet } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import { Button, makeStyles, Typography } from '@material-ui/core'; +import React from 'react'; + +const ENTITY_YAML = `metadata: + name: example + links: + - url: https://dashboard.example.com + title: My Dashboard + icon: dashboard`; + +const useStyles = makeStyles(theme => ({ + code: { + borderRadius: 6, + margin: `${theme.spacing(2)}px 0px`, + background: theme.palette.type === 'dark' ? '#444' : '#fff', + }, +})); + +export const EntityLinksEmptyState = () => { + const classes = useStyles(); + + return ( + <> + + No links defined for this entity. You can add links to your entity YAML + as shown in the highlighted example below: + + + + + + Read more + + > + ); +}; diff --git a/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx b/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx new file mode 100644 index 0000000000..8caf37e70d --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/IconLink.test.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 { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import CloudIcon from '@material-ui/icons/Cloud'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { IconLink } from './IconLink'; + +describe('IconLink', () => { + it('should render an icon link', () => { + const rendered = render( + + + , + ); + + expect(rendered.queryByText('I am Link')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx b/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx new file mode 100644 index 0000000000..32ea085889 --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/IconLink.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. + */ + +import { Link, IconComponent } from '@backstage/core'; +import { Grid, makeStyles, Typography } from '@material-ui/core'; +import LanguageIcon from '@material-ui/icons/Language'; +import React from 'react'; + +const useStyles = makeStyles({ + svgIcon: { + display: 'inline-block', + '& svg': { + display: 'inline-block', + fontSize: 'inherit', + verticalAlign: 'baseline', + }, + }, +}); + +export const IconLink = ({ + href, + text, + Icon, +}: { + href: string; + text?: string; + Icon?: IconComponent; +}) => { + const classes = useStyles(); + + return ( + + + + {Icon ? : } + + + + + {text || href} + + + + ); +}; diff --git a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx new file mode 100644 index 0000000000..9ef77a4bbf --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx @@ -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 { IconComponent } from '@backstage/core'; +import { GridList, GridListTile } from '@material-ui/core'; +import React from 'react'; +import { IconLink } from './IconLink'; +import { ColumnBreakpoints } from './types'; +import { useDynamicColumns } from './useDynamicColumns'; + +export type LinksGridListItem = { + href: string; + text?: string; + Icon?: IconComponent; +}; + +type Props = { + items: LinksGridListItem[]; + cols?: ColumnBreakpoints | number; +}; + +export const LinksGridList = ({ items, cols = undefined }: Props) => { + const numOfCols = useDynamicColumns(cols); + + return ( + + {items.map(({ text, href, Icon }, i) => ( + + + + ))} + + ); +}; diff --git a/plugins/catalog-import/src/util/validate.ts b/plugins/catalog/src/components/EntityLinksCard/index.ts similarity index 76% rename from plugins/catalog-import/src/util/validate.ts rename to plugins/catalog/src/components/EntityLinksCard/index.ts index 056131aaae..d893d3a233 100644 --- a/plugins/catalog-import/src/util/validate.ts +++ b/plugins/catalog/src/components/EntityLinksCard/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -export const ComponentIdValidators = { - httpsValidator: (value: any) => - (typeof value === 'string' && value.match(/^https:\/\//) !== null) || - 'Must start with https://.', -}; +export { EntityLinksCard } from './EntityLinksCard'; diff --git a/plugins/catalog/src/components/EntityLinksCard/types.ts b/plugins/catalog/src/components/EntityLinksCard/types.ts new file mode 100644 index 0000000000..d7729fc216 --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/types.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 type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export type ColumnBreakpoints = Record; diff --git a/plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.tsx b/plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.tsx new file mode 100644 index 0000000000..d2b4168e0b --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.tsx @@ -0,0 +1,49 @@ +/* + * 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 { Theme, useMediaQuery } from '@material-ui/core'; +import { Breakpoint, ColumnBreakpoints } from './types'; + +const colDefaults: ColumnBreakpoints = { + xs: 1, + sm: 1, + md: 1, + lg: 2, + xl: 3, +}; + +export function useDynamicColumns( + cols: ColumnBreakpoints | number | undefined, +): number { + const matches: (Breakpoint | null)[] = [ + useMediaQuery((theme: Theme) => theme.breakpoints.up('xl')) ? 'xl' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('lg')) ? 'lg' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) ? 'md' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')) ? 'sm' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('xs')) ? 'xs' : null, + ]; + + let numOfCols = 1; + + if (typeof cols === 'number') { + numOfCols = cols; + } else { + const breakpoint = matches.find(k => k !== null) ?? 'xs'; + numOfCols = cols?.[breakpoint] ?? colDefaults[breakpoint]; + } + + return numOfCols; +} diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 06ea1eee27..ef4196e9c5 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -110,7 +110,7 @@ export const UnregisterEntityDialog = ({ {error.name === 'DeniedLocationException' ? ( <> You cannot unregister this entity, since it originates from a - protected Backstage configuration (location + protected Backstage configuration (location{' '} {`"${(error as DeniedLocationException).locationName}"`}). If you believe this is in error, please contact the{' '} {configApi.getOptionalString('app.title') ?? 'Backstage'}{' '} diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.ts b/plugins/catalog/src/filter/useEntityFilterGroup.ts index 242238e4f4..30214fad78 100644 --- a/plugins/catalog/src/filter/useEntityFilterGroup.ts +++ b/plugins/catalog/src/filter/useEntityFilterGroup.ts @@ -43,15 +43,19 @@ export const useEntityFilterGroup = ( filterGroupStates, } = context; - // Intentionally consider initial set only at mount time + // on state changes unregisters and registers the filtergroup + // ensure that it re-registers with the correct filter as the prop changes and not the default // eslint-disable-next-line react-hooks/exhaustive-deps - const initialMemo = useMemo(() => initialSelectedFilters?.slice(), []); + const initialMemo = useMemo(() => { + return initialSelectedFilters?.slice(); + }, [initialSelectedFilters]); // Register the group on mount, and unregister on unmount useEffect(() => { register(filterGroupId, filterGroup, initialMemo); return () => unregister(filterGroupId); - }, [register, unregister, filterGroupId, filterGroup, initialMemo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [register, unregister, filterGroupId, filterGroup]); const setSelectedFilters = useCallback( (filters: string[]) => { diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 3ef79680c4..1bc11f6880 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -24,4 +24,5 @@ export { catalogPlugin as plugin, CatalogIndexPage, CatalogEntityPage, + EntityLinksCard, } from './plugin'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index c19925775d..c6944a318a 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -19,21 +19,28 @@ import { createApiFactory, createPlugin, discoveryApiRef, + createComponentExtension, createRoutableExtension, + identityApiRef, } from '@backstage/core'; import { catalogApiRef, catalogRouteRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; export const catalogPlugin = createPlugin({ id: 'catalog', apis: [ createApiFactory({ api: catalogApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new CatalogClientWrapper({ + client: new CatalogClient({ discoveryApi }), + identityApi, + }), }), ], routes: { @@ -59,3 +66,12 @@ export const CatalogEntityPage = catalogPlugin.provide( mountPoint: entityRouteRef, }), ); + +export const EntityLinksCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/EntityLinksCard').then(m => m.EntityLinksCard), + }, + }), +); diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 99715821a6..1c5d35aa70 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-circleci +## 0.2.8 + +### Patch Changes + +- 6b26c9f41: Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 37e4425b23..6f741ab1ce 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,8 +51,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index ae3f8faada..8dccd1dbbd 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cloudbuild +## 0.2.9 + +### Patch Changes + +- 302795d10: Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.8 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index cbad4928b7..a00fe13fe0 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.8", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 87bfd3af07..e672b33976 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cost-insights +## 0.8.1 + +### Patch Changes + +- b33fa4cf4: fixes a bug in default dismiss form where other text input persists between reason selections +- d36660721: Fix snooze quarter option +- 02d6803e8: Migrated to new composability API, exporting the plugin instance as `costInsightsPlugin`, the root `'/cost-insights'` page as `CostInsightsPage`, the `'/cost-insights/investigating-growth'` page as `CostInsightsProjectGrowthInstructionsPage`, and the `'/cost-insights/labeling-jobs'` page as `CostInsightsLabelDataflowInstructionsPage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.8.0 ### Minor Changes diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index bdbb4450fa..e4732fc364 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -16,10 +16,10 @@ import { createDevApp } from '@backstage/dev-utils'; import { costInsightsApiRef } from '../src/api'; import { ExampleCostInsightsClient } from '../src/client'; -import { plugin } from '../src/plugin'; +import { costInsightsPlugin } from '../src/plugin'; createDevApp() - .registerPlugin(plugin) + .registerPlugin(costInsightsPlugin) .registerApi({ api: costInsightsApiRef, deps: {}, diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 0184d50517..2f0511c217 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.8.0", + "version": "0.8.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -56,8 +56,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/src/forms/AlertDismissForm.tsx b/plugins/cost-insights/src/forms/AlertDismissForm.tsx index 4271785958..47b4dc80cb 100644 --- a/plugins/cost-insights/src/forms/AlertDismissForm.tsx +++ b/plugins/cost-insights/src/forms/AlertDismissForm.tsx @@ -66,6 +66,9 @@ export const AlertDismissForm = forwardRef< }; const onReasonChange = (_: ChangeEvent, value: string) => { + if (other) { + setOther(null); + } setReason(value as AlertDismissReason); }; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 13ce929401..0ae9d05b09 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + costInsightsPlugin, + costInsightsPlugin as plugin, + CostInsightsPage, + CostInsightsProjectGrowthInstructionsPage, + CostInsightsLabelDataflowInstructionsPage, +} from './plugin'; export * from './client'; export * from './api'; export { ProjectGrowthAlert, UnlabeledDataflowAlert } from './alerts'; diff --git a/plugins/cost-insights/src/plugin.test.ts b/plugins/cost-insights/src/plugin.test.ts index 6210e11d4c..87be3ff8a2 100644 --- a/plugins/cost-insights/src/plugin.test.ts +++ b/plugins/cost-insights/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { costInsightsPlugin } from './plugin'; describe('cost-insights', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(costInsightsPlugin).toBeDefined(); }); }); diff --git a/plugins/cost-insights/src/plugin.ts b/plugins/cost-insights/src/plugin.ts index 7006d5bf5d..29e345532d 100644 --- a/plugins/cost-insights/src/plugin.ts +++ b/plugins/cost-insights/src/plugin.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; -import { CostInsightsPage } from './components/CostInsightsPage'; -import { ProjectGrowthInstructionsPage } from './components/ProjectGrowthInstructionsPage'; -import { LabelDataflowInstructionsPage } from './components/LabelDataflowInstructionsPage'; +import { + createPlugin, + createRouteRef, + createRoutableExtension, +} from '@backstage/core'; +import { CostInsightsPage as CostInsightsPageComponent } from './components/CostInsightsPage'; +import { ProjectGrowthInstructionsPage as ProjectGrowthInstructionsPageComponent } from './components/ProjectGrowthInstructionsPage'; +import { LabelDataflowInstructionsPage as LabelDataflowInstructionsPageComponent } from './components/LabelDataflowInstructionsPage'; export const rootRouteRef = createRouteRef({ path: '/cost-insights', @@ -34,12 +38,51 @@ export const unlabeledDataflowAlertRef = createRouteRef({ title: 'Labeling Dataflow Jobs', }); -export const plugin = createPlugin({ +export const costInsightsPlugin = createPlugin({ id: 'cost-insights', register({ router, featureFlags }) { - router.addRoute(rootRouteRef, CostInsightsPage); - router.addRoute(projectGrowthAlertRef, ProjectGrowthInstructionsPage); - router.addRoute(unlabeledDataflowAlertRef, LabelDataflowInstructionsPage); + router.addRoute(rootRouteRef, CostInsightsPageComponent); + router.addRoute( + projectGrowthAlertRef, + ProjectGrowthInstructionsPageComponent, + ); + router.addRoute( + unlabeledDataflowAlertRef, + LabelDataflowInstructionsPageComponent, + ); featureFlags.register('cost-insights-currencies'); }, + routes: { + root: rootRouteRef, + growthAlerts: projectGrowthAlertRef, + unlabeledDataflowAlerts: unlabeledDataflowAlertRef, + }, }); + +export const CostInsightsPage = costInsightsPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/CostInsightsPage').then(m => m.CostInsightsPage), + mountPoint: rootRouteRef, + }), +); + +export const CostInsightsProjectGrowthInstructionsPage = costInsightsPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ProjectGrowthInstructionsPage').then( + m => m.ProjectGrowthInstructionsPage, + ), + mountPoint: projectGrowthAlertRef, + }), +); + +export const CostInsightsLabelDataflowInstructionsPage = costInsightsPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/LabelDataflowInstructionsPage').then( + m => m.LabelDataflowInstructionsPage, + ), + mountPoint: unlabeledDataflowAlertRef, + }), +); diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 0d2daf37b5..b0334effe1 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-explore +## 0.2.5 + +### Patch Changes + +- 0fe8ff5be: Catch catalog errors and display to user +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.4 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index e9d21cc221..497e108b6f 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/plugin-explore-react": "^0.0.2", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -46,8 +46,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index 4846565bdd..a52b0d8f2c 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -90,4 +90,17 @@ describe('', () => { expect(getByText('No domains to display')).toBeInTheDocument(), ); }); + + it('renders a friendly error if it cannot collect domains', async () => { + const catalogError = new Error('Network timeout'); + catalogApi.getEntities.mockRejectedValueOnce(catalogError); + + const { getByText } = render(, { + wrapper: Wrapper, + }); + + await waitFor(() => + expect(getByText(/Could not load domains/)).toBeInTheDocument(), + ); + }); }); diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index ca00c525c9..182adf70ea 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -21,6 +21,7 @@ import { Progress, SupportButton, useApi, + WarningPanel, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; @@ -30,7 +31,7 @@ import { DomainCardGrid } from '../DomainCard'; export const DomainExplorerContent = () => { const catalogApi = useApi(catalogApiRef); - const { value: entities, loading } = useAsync(async () => { + const { value: entities, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities({ filter: { kind: 'domain' }, }); @@ -45,7 +46,12 @@ export const DomainExplorerContent = () => { {loading && } - {!loading && (!entities || entities.length === 0) && ( + {error && ( + + {error.message} + + )} + {!loading && !error && (!entities || entities.length === 0) && ( { msw.setupDefaultHandlers(server); @@ -30,7 +45,11 @@ describe('FossaClient', () => { let client: FossaApi; beforeEach(() => { - client = new FossaClient({ discoveryApi, organizationId: '8736' }); + client = new FossaClient({ + discoveryApi, + identityApi, + organizationId: '8736', + }); }); it('should report finding summary', async () => { @@ -137,7 +156,7 @@ describe('FossaClient', () => { }); it('should skip organizationId', async () => { - client = new FossaClient({ discoveryApi }); + client = new FossaClient({ discoveryApi, identityApi }); server.use( rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 05a889177a..5693895901 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -14,28 +14,35 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; import fetch from 'cross-fetch'; import { FindingSummary, FossaApi } from './FossaApi'; export class FossaClient implements FossaApi { discoveryApi: DiscoveryApi; + identityApi: IdentityApi; organizationId?: string; constructor({ discoveryApi, + identityApi, organizationId, }: { discoveryApi: DiscoveryApi; + identityApi: IdentityApi; organizationId?: string; }) { this.discoveryApi = discoveryApi; + this.identityApi = identityApi; this.organizationId = organizationId; } private async callApi(path: string): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const response = await fetch(`${apiUrl}/${path}`); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(`${apiUrl}/${path}`, { + headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, + }); if (response.status === 200) { return await response.json(); } diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts index cb1de3911f..638be4a0bf 100644 --- a/plugins/fossa/src/plugin.ts +++ b/plugins/fossa/src/plugin.ts @@ -19,6 +19,7 @@ import { createApiFactory, createPlugin, discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { fossaApiRef, FossaClient } from './api'; @@ -27,10 +28,15 @@ export const fossaPlugin = createPlugin({ apis: [ createApiFactory({ api: fossaApiRef, - deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, - factory: ({ configApi, discoveryApi }) => + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, discoveryApi, identityApi }) => new FossaClient({ discoveryApi, + identityApi, organizationId: configApi.getOptionalString('fossa.organizationId'), }), }), diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index fced060d99..36dc403d7b 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index a9bbf67a9a..8aba49fedc 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-actions +## 0.3.2 + +### Patch Changes + +- f5f45744e: Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGithubActionsContent`, entity conditional as `isGithubActionsAvailable`, and entity cards as `EntityLatestGithubActionRunCard`, `EntityLatestGithubActionsForBranchCard`, and `EntityRecentGithubActionsRunsCard`. +- Updated dependencies [ffffea8e6] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/integration@0.4.0 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 7d05395b42..e5498563a5 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", - "@backstage/integration": "^0.3.2", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.1", + "@backstage/integration": "^0.4.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,8 +51,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 0fa9fa24a2..bb0834a03d 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-gitops-profiles +## 0.2.5 + +### Patch Changes + +- accdfeb30: Migrated to new composability API, exporting the plugin instance as `gitopsProfilesPlugin` and pages as `GitopsProfilesClusterListPage`, `GitopsProfilesClusterPage`, and `GitopsProfilesCreatePage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.2.4 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index c25628fa62..251185b799 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,8 +43,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 9293a07dd7..307cef0dee 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 2199c58949..26c838d97a 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-jenkins +## 0.3.9 + +### Patch Changes + +- 53348f0af: Improve display of duration in latest build card +- 025c0c7bf: Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.8 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 5d59d2e5ab..fb3e719858 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.8", + "version": "0.3.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index 447afff266..6fc544bcea 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -40,13 +40,13 @@ const WidgetContent = ({ const classes = useStyles(); if (loading || !latestRun) return ; const displayDate = DateTime.fromMillis(latestRun.timestamp).toRelative(); - // TODO This works, but hard codes as minutes. Would prefer something smarter/relative. - const durationInMinutes = Math.round( - Duration.fromMillis(latestRun.duration).as('minutes'), - ); - const displayDuration = `${durationInMinutes} minute${ - durationInMinutes > 1 ? 's' : '' - }`; + const displayDuration = + (latestRun.building ? 'Running for ' : '') + + DateTime.local() + .minus(Duration.fromMillis(latestRun.duration)) + .toRelative({ locale: 'en' }) + ?.replace(' ago', ''); + return ( { const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; + const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), }, body: JSON.stringify(requestBody), }); diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index af29df3dcc..24f51fe64a 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + identityApiRef, googleAuthApiRef, createRoutableExtension, } from '@backstage/core'; @@ -36,9 +37,9 @@ export const kubernetesPlugin = createPlugin({ apis: [ createApiFactory({ api: kubernetesApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => - new KubernetesBackendClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new KubernetesBackendClient({ discoveryApi, identityApi }), }), createApiFactory({ api: kubernetesAuthProvidersApiRef, diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 8fec8887c3..fab61283d9 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-lighthouse +## 0.2.10 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.9 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 07e1ad8475..064e260389 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 6eae73bc86..ab8ad1c289 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-newrelic +## 0.2.5 + +### Patch Changes + +- c5ab91ce3: Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.2.4 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index ccd43c42c8..6ab18a9cf5 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 6101685e1d..898705cdc4 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-org +## 0.3.6 + +### Patch Changes + +- 14aef4b94: Visual updates to User and Group pages +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.5 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 0491554f77..4e12ad8837 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,8 +34,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 2f0f1e2e16..9a72606c16 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,8 +21,21 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; -import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import { + getEntityRelations, + entityRouteParams, + useEntity, +} from '@backstage/plugin-catalog-react'; +import { + Box, + Grid, + Link, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, +} from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; @@ -80,9 +93,9 @@ export const GroupProfileCard = ({ ?.map(groupItem => groupItem.target.name) .toString(); - const childrens = group?.relations - ?.filter(r => r.type === RELATION_PARENT_OF) - ?.map(groupItem => groupItem.target.name); + const childRelations = getEntityRelations(group, RELATION_PARENT_OF, { + kind: 'group', + }); const displayName = profile?.displayName ?? name; @@ -96,61 +109,51 @@ export const GroupProfileCard = ({ > - - - + - {profile?.email && ( - - - - - - - - {profile.email} - - - - )} - {parent ? ( - - - - - - + + {profile?.email && ( + + + + + + + {profile.email} + + )} + {parent ? ( + + + + + + + - - - - ) : null} - {childrens?.length ? ( - - - - - - - {childrens.map((children, index) => ( + + + ) : null} + {childRelations?.length ? ( + + + + + + + + {childRelations.map((children, index) => ( ))} - - - - ) : null} + + + ) : null} + diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 1cf7c968b3..a4969f615b 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,8 +19,17 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; -import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import { entityRouteParams, useEntity } from '@backstage/plugin-catalog-react'; +import { + Box, + Grid, + Link, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, +} from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import PersonIcon from '@material-ui/icons/Person'; @@ -81,40 +90,35 @@ export const UserProfileCard = ({ return User not found; } + const emailHref = profile?.email ? `mailto:${profile.email}` : ''; + return ( } variant={variant}> - - - - - + + + - - {profile?.email && ( - - - - - - - {profile.email} - - - - )} - - - - - - + + + {profile?.email && ( + + + + + + {profile.email} + + + )} + + + + + + + + {groupNames.map((groupName, index) => ( ))} - - - + + + diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index eb00ce36be..3fe8e6db70 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-pagerduty +## 0.2.8 + +### Patch Changes + +- 29a138636: Use the Luxon Date Library to follow the recommendations of ADR010. +- b288a291e: Migrated to new composability API, exporting the plugin instance as `pagerDutyPlugin`, entity card as `EntityPagerDutyCard`, and entity conditional as `isPagerDutyAvailable`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index afd41dcc46..c430075b95 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "classnames": "^2.2.6", - "date-fns": "^2.15.0", + "luxon": "1.25.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", @@ -46,16 +46,16 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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", + "cross-fetch": "^3.0.6", "msw": "^0.21.2", - "node-fetch": "^2.6.1", - "cross-fetch": "^3.0.6" + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index 860623dcd4..bf0d5d2587 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -27,7 +27,7 @@ import { Typography, } from '@material-ui/core'; import { StatusError, StatusWarning } from '@backstage/core'; -import { formatDistanceToNowStrict } from 'date-fns'; +import { DateTime, Duration } from 'luxon'; import { Incident } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; @@ -53,8 +53,12 @@ type Props = { export const IncidentListItem = ({ incident }: Props) => { const classes = useStyles(); + const duration = + new Date().getTime() - new Date(incident.created_at).getTime(); + const createdAt = DateTime.local() + .minus(Duration.fromMillis(duration)) + .toRelative({ locale: 'en' }); const user = incident.assignments[0]?.assignee; - const createdAt = formatDistanceToNowStrict(new Date(incident.created_at)); return ( @@ -77,7 +81,7 @@ export const IncidentListItem = ({ incident }: Props) => { }} secondary={ - Created {createdAt} ago and assigned to{' '} + Created {createdAt} and assigned to{' '} { @@ -53,7 +58,10 @@ export class RollbarClient implements RollbarApi { private async get(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; - const response = await fetch(url); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(url, { + headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, + }); if (!response.ok) { const payload = await response.text(); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts index 216713b3fc..dc0ffa259a 100644 --- a/plugins/rollbar/src/plugin.ts +++ b/plugins/rollbar/src/plugin.ts @@ -20,6 +20,7 @@ import { createRoutableExtension, createRouteRef, discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { rollbarApiRef } from './api/RollbarApi'; import { RollbarClient } from './api/RollbarClient'; @@ -34,8 +35,9 @@ export const rollbarPlugin = createPlugin({ apis: [ createApiFactory({ api: rollbarApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new RollbarClient({ discoveryApi, identityApi }), }), ], routes: { diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index e8526e0b56..5a3553dc79 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder-backend +## 0.7.0 + +### Minor Changes + +- 615103a63: Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend. + + This API should be considered unstable until used by the scaffolder frontend. + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- ffffea8e6: Minor updates to reflect the changes in `@backstage/integration` that made the fields `apiBaseUrl` and `apiUrl` mandatory. +- Updated dependencies [6ed2b47d6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [72b96e880] +- Updated dependencies [5a5163519] + - @backstage/catalog-client@0.3.6 + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + ## 0.6.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ecce161c8e..3b25fb3881 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.6.0", + "version": "0.7.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", @@ -61,7 +62,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.7", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml index 36404f088c..168daffd20 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml @@ -22,6 +22,7 @@ spec: component_id: title: Name type: string + pattern: ^[a-z0-9A-Z_.-]{1,63}$ description: Unique name of the component description: title: Description diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 4541f03a4a..827ed0559a 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { CatalogClient } from '@backstage/catalog-client'; import { ConflictError, NotFoundError, @@ -26,10 +26,12 @@ import { * A catalog client tailored for reading out entity data from the catalog. */ export class CatalogEntityClient { - private readonly discovery: PluginEndpointDiscovery; + private readonly catalogClient: CatalogClient; constructor(options: { discovery: PluginEndpointDiscovery }) { - this.discovery = options.discovery; + this.catalogClient = new CatalogClient({ + discoveryApi: options.discovery, + }); } /** @@ -37,25 +39,19 @@ export class CatalogEntityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ - async findTemplate(templateName: string): Promise { - const conditions = [ - 'kind=template', - `metadata.name=${encodeURIComponent(templateName)}`, - ]; - - const baseUrl = await this.discovery.getBaseUrl('catalog'); - const response = await fetch( - `${baseUrl}/entities?filter=${conditions.join(',')}`, - ); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `Request failed with ${response.status} ${response.statusText}, ${text}`, - ); - } - - const templates: TemplateEntityV1alpha1[] = await response.json(); + async findTemplate( + templateName: string, + options?: { token?: string }, + ): Promise { + const { items: templates } = (await this.catalogClient.getEntities( + { + filter: { + kind: 'template', + 'metadata.name': templateName, + }, + }, + options, + )) as { items: TemplateEntityV1alpha1[] }; if (templates.length !== 1) { if (templates.length > 1) { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index d473656863..02865f8529 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -43,7 +43,7 @@ export class BitbucketPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); const git = Git.fromAuth({ logger, ...this.getAuth() }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 533aa8e005..77e6ec6d70 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -33,7 +33,7 @@ export class GithubPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); const git = this.config.token diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 6ed4651e4f..2de2326505 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -37,8 +37,10 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); const preparer = GitlabPreparer.fromConfig({ - host: 'gitlab.com', + host: '', token: 'fake-token', + apiBaseUrl: '', + baseUrl: '', }); it(`calls the clone command with the correct arguments for a repository`, async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index fa5c8c1325..e15de33ac6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -33,7 +33,7 @@ export class GitlabPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); parsedGitUrl.git_suffix = true; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index 61c8480efb..3826a54ddb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -57,6 +57,7 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig( { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', token: 'fake-token', baseUrl: 'https://gitlab.hosted.com', }, @@ -105,7 +106,9 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig( { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', token: 'fake-token', + baseUrl: 'https://gitlab.com', }, { repoVisibility: 'public' }, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 30201e6dca..f5f7fc6361 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -126,6 +126,8 @@ export class Publishers implements PublisherBuilder { { token: config.getOptionalString('scaffolder.gitlab.token') ?? '', host: integration.config.host, + apiBaseUrl: ``, + baseUrl: `https://${integration.config.host}`, }, { repoVisibility }, ), diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5821edfe8c..51425f75e8 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -133,7 +133,10 @@ export async function createRouter( }, }; - const template = await entityClient.findTemplate(templateName); + // Forward authorization from client + const template = await entityClient.findTemplate(templateName, { + token: getBearerToken(req.headers.authorization), + }); const validationResult: ValidatorResult = validate( values, @@ -295,3 +298,7 @@ export async function createRouter( return app; } + +function getBearerToken(header?: string): string | undefined { + return header?.match(/Bearer\s+(\S+)/i)?.[1]; +} diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index cd7c8fe685..2e0676e3aa 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder +## 0.5.0 + +### Minor Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. + +### Patch Changes + +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.4.2 ### Patch Changes diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index eff16ab0f5..e75aadb03f 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { discoveryApiRef } from '@backstage/core'; +import { discoveryApiRef, identityApiRef } from '@backstage/core'; import { CatalogClient } from '@backstage/catalog-client'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { TemplateIndexPage, TemplatePage } from '../src/plugin'; @@ -30,8 +30,9 @@ createDevApp() }) .registerApi({ api: scaffolderApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new ScaffolderApi({ discoveryApi, identityApi }), }) .addPage({ path: '/create', diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 56beb7b10b..2b0e1a3771 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.4.2", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -52,9 +52,9 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", - "@backstage/catalog-client": "^0.3.5", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", + "@backstage/catalog-client": "^0.3.6", "@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 ca52418e92..453b1b6a7e 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core'; +import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', @@ -23,9 +23,14 @@ export const scaffolderApiRef = createApiRef({ export class ScaffolderApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; } /** @@ -36,11 +41,13 @@ export class ScaffolderApi { * @param values Parameters for the template, e.g. name, description */ async scaffold(templateName: string, values: Record) { + const token = await this.identityApi.getIdToken(); const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), }, body: JSON.stringify({ templateName, values: { ...values } }), }); @@ -56,8 +63,11 @@ export class ScaffolderApi { } async getJob(jobId: string) { + const token = await this.identityApi.getIdToken(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v1/job/${encodeURIComponent(jobId)}`; - return fetch(url).then(x => x.json()); + return fetch(url, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }).then(x => x.json()); } } diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ed20705185..3e884d7683 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createApiFactory, discoveryApiRef, + identityApiRef, createRoutableExtension, } from '@backstage/core'; import { ScaffolderPage as ScaffolderPageComponent } from './components/ScaffolderPage'; @@ -30,8 +31,9 @@ export const scaffolderPlugin = createPlugin({ apis: [ createApiFactory({ api: scaffolderApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new ScaffolderApi({ discoveryApi, identityApi }), }), ], register({ router }) { diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 383cb3c484..38cbf202e1 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search +## 0.3.0 + +### Minor Changes + +- b3f0c3811: Migrated to new composability API, exporting the plugin instance as `searchPlugin`, and page as `SearchPage`. Due to the old router component also being called `SearchPage`, this is a breaking change. The old page component is now exported as `Router`, which can be used to maintain the old behavior. + +### Patch Changes + +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 6142e65dd3..56f863a2a6 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.2.7", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 698f72020c..4c4d68c67b 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.3.5 + +### Patch Changes + +- 53d3e2d62: Export the plugin instance as `sentryPlugin`. The plugin instance is still exported as `plugin` as well, but it will be removed in the future. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.4 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 06b538289b..b79b7613fe 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry/src/extensions.tsx b/plugins/sentry/src/extensions.tsx index ea63cd472a..9a16a6a7da 100644 --- a/plugins/sentry/src/extensions.tsx +++ b/plugins/sentry/src/extensions.tsx @@ -20,9 +20,9 @@ import { } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; -import { plugin, rootRouteRef } from './plugin'; +import { sentryPlugin, rootRouteRef } from './plugin'; -export const EntitySentryContent = plugin.provide( +export const EntitySentryContent = sentryPlugin.provide( createRoutableExtension({ mountPoint: rootRouteRef, component: () => @@ -38,7 +38,7 @@ export const EntitySentryContent = plugin.provide( }), ); -export const EntitySentryCard = plugin.provide( +export const EntitySentryCard = sentryPlugin.provide( createComponentExtension({ component: { lazy: () => diff --git a/plugins/sentry/src/index.ts b/plugins/sentry/src/index.ts index de53d7456a..2fc51715f6 100644 --- a/plugins/sentry/src/index.ts +++ b/plugins/sentry/src/index.ts @@ -16,6 +16,6 @@ export * from './api'; export * from './components'; -export { plugin } from './plugin'; +export { sentryPlugin, sentryPlugin as plugin } from './plugin'; export { EntitySentryCard, EntitySentryContent } from './extensions'; export { Router } from './components/Router'; diff --git a/plugins/sentry/src/plugin.test.ts b/plugins/sentry/src/plugin.test.ts index 8f24236586..af5feaa774 100644 --- a/plugins/sentry/src/plugin.test.ts +++ b/plugins/sentry/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { sentryPlugin } from './plugin'; describe('sentry', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(sentryPlugin).toBeDefined(); }); }); diff --git a/plugins/sentry/src/plugin.ts b/plugins/sentry/src/plugin.ts index 49ecfdb5f2..4c2a00de59 100644 --- a/plugins/sentry/src/plugin.ts +++ b/plugins/sentry/src/plugin.ts @@ -28,7 +28,7 @@ export const rootRouteRef = createRouteRef({ title: 'Sentry', }); -export const plugin = createPlugin({ +export const sentryPlugin = createPlugin({ id: 'sentry', apis: [ createApiFactory({ diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index c422f9d72e..11306bb5cd 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-sonarqube +## 0.1.11 + +### Patch Changes + +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.1.10 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index a31ee320f2..e8236fbfd6 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.10", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 49a5f8bfcd..3c4e3a0706 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-tech-radar +## 0.3.5 + +### Patch Changes + +- 804502a5c: Migrated to new composability API, exporting the plugin instance as `techRadarPlugin` and the page as `TechRadarPage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.3.4 ### Patch Changes diff --git a/plugins/tech-radar/dev/index.tsx b/plugins/tech-radar/dev/index.tsx index 92eb6da567..0e9495394f 100644 --- a/plugins/tech-radar/dev/index.tsx +++ b/plugins/tech-radar/dev/index.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ +import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src'; +import { techRadarPlugin, TechRadarPage } from '../src'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerPlugin(techRadarPlugin) + .addPage({ + title: 'Tech Radar', + element: , + }) + .render(); diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index a5c9f7bb53..7a7c886372 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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/src/index.ts b/plugins/tech-radar/src/index.ts index d7b4921e9a..b13e0fbeed 100644 --- a/plugins/tech-radar/src/index.ts +++ b/plugins/tech-radar/src/index.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + techRadarPlugin, + techRadarPlugin as plugin, + TechRadarPage, +} from './plugin'; export { RadarPage as Router } from './components/RadarPage'; diff --git a/plugins/tech-radar/src/plugin.test.ts b/plugins/tech-radar/src/plugin.test.ts index 6ae65f8271..f3f2fdbd77 100644 --- a/plugins/tech-radar/src/plugin.test.ts +++ b/plugins/tech-radar/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { techRadarPlugin } from './plugin'; describe('tech-radar', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(techRadarPlugin).toBeDefined(); }); }); diff --git a/plugins/tech-radar/src/plugin.ts b/plugins/tech-radar/src/plugin.ts index d8dc41af2e..63af1f390f 100644 --- a/plugins/tech-radar/src/plugin.ts +++ b/plugins/tech-radar/src/plugin.ts @@ -14,8 +14,26 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; +import { + createPlugin, + createRouteRef, + createRoutableExtension, +} from '@backstage/core'; -export const plugin = createPlugin({ - id: 'tech-radar', +const rootRouteRef = createRouteRef({ + title: 'Tech Radar', }); + +export const techRadarPlugin = createPlugin({ + id: 'tech-radar', + routes: { + root: rootRouteRef, + }, +}); + +export const TechRadarPage = techRadarPlugin.provide( + createRoutableExtension({ + component: () => import('./components/RadarPage').then(m => m.RadarPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 28ba4e032c..50925cec5f 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-techdocs-backend +## 0.6.0 + +### Minor Changes + +- 08142b256: URL Preparer will now use proper etag based caching introduced in https://github.com/backstage/backstage/pull/4120. Previously, builds used to be cached for 30 minutes. + +### Patch Changes + +- 08142b256: TechDocs will throw warning in backend logs when legacy git preparer or dir preparer is used to preparer docs. Migrate to URL Preparer by updating `backstage.io/techdocs-ref` annotation to be prefixed with `url:`. + Detailed docs are here https://backstage.io/docs/features/techdocs/how-to-guides#how-to-use-url-reader-in-techdocs-prepare-step + See benefits and reason for doing so https://github.com/backstage/backstage/issues/4409 +- Updated dependencies [77ad0003a] +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [5a5163519] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] + - @backstage/techdocs-common@0.4.0 + - @backstage/backend-common@0.5.3 + ## 0.5.5 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 36e5bd3bcd..2d4d1309c0 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.5.5", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/techdocs-common": "^0.3.7", + "@backstage/techdocs-common": "^0.4.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", diff --git a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts index 87de9d1d02..54ea860fb9 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts @@ -17,11 +17,11 @@ import { BuildMetadataStorage } from './BuildMetadataStorage'; describe('BuildMetadataStorage', () => { it('should return build timestamp', () => { - const newMetadataStorage = new BuildMetadataStorage('123abc'); - newMetadataStorage.storeBuildTimestamp(); + const newMetadataStorage = new BuildMetadataStorage('entityID123abc'); + newMetadataStorage.setEtag('etag123abc'); - const timestamp = newMetadataStorage.getTimestamp(); + const timestamp = newMetadataStorage.getEtag(); - expect(timestamp).toBeLessThanOrEqual(Date.now()); + expect(timestamp).toBe('etag123abc'); }); }); diff --git a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts index d6a19764d9..a1bb918fae 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts @@ -14,10 +14,15 @@ * limitations under the License. */ type buildInfo = { - // uid: timestamp - [key: string]: number; + // Entity uid: etag + [key: string]: string; }; +// TODO: Build info should be part of TechDocs storage, inside `techdocs_metadata.json` +// instead of in-memory storage of the Backstage instance. +// In case of multi-region Backstage deployments, or even using multiple Kubernetes pods, +// if each instance creates its separate build info in-memory, it will result in duplicate +// builds per instance. Also if the pod restarts, all the sites will have to be re-built. const builds = {} as buildInfo; /** @@ -34,11 +39,11 @@ export class BuildMetadataStorage { this.builds = builds; } - storeBuildTimestamp() { - this.builds[this.entityUid] = Date.now(); + setEtag(etag: string): void { + this.builds[this.entityUid] = etag; } - getTimestamp() { + getEtag(): string | undefined { return this.builds[this.entityUid]; } } diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 33d3120c47..8c54a2b1e4 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { NotModifiedError } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { + GeneratorBase, + GeneratorBuilder, + getLocationForEntity, + PreparerBase, + PreparerBuilder, + PublisherBase, + UrlPreparer, +} from '@backstage/techdocs-common'; +import Docker from 'dockerode'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; -import Docker from 'dockerode'; import { Logger } from 'winston'; -import { Entity } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { - PreparerBuilder, - PublisherBase, - GeneratorBuilder, - PreparerBase, - GeneratorBase, - getLocationForEntity, - getLastCommitTimestamp, -} from '@backstage/techdocs-common'; import { BuildMetadataStorage } from '.'; const getEntityId = (entity: Entity) => { @@ -44,7 +44,6 @@ type DocsBuilderArguments = { entity: Entity; logger: Logger; dockerClient: Docker; - config: Config; }; export class DocsBuilder { @@ -54,7 +53,6 @@ export class DocsBuilder { private entity: Entity; private logger: Logger; private dockerClient: Docker; - private config: Config; constructor({ preparers, @@ -63,7 +61,6 @@ export class DocsBuilder { entity, logger, dockerClient, - config, }: DocsBuilderArguments) { this.preparer = preparers.get(entity); this.generator = generators.get(entity); @@ -71,14 +68,53 @@ export class DocsBuilder { this.entity = entity; this.logger = logger; this.dockerClient = dockerClient; - this.config = config; } - public async build() { - this.logger.info(`Running preparer on entity ${getEntityId(this.entity)}`); - const preparedDir = await this.preparer.prepare(this.entity); + public async build(): Promise { + if (!this.entity.metadata.uid) { + throw new Error( + 'Trying to build documentation for entity not in service catalog', + ); + } - const parsedLocationAnnotation = getLocationForEntity(this.entity); + /** + * Prepare and cache check + */ + + // Use the in-memory storage for setting and getting etag for this entity. + const buildMetadataStorage = new BuildMetadataStorage( + this.entity.metadata.uid, + ); + + // TODO: As of now, this happens on each and every request to TechDocs. + // In a high traffic environment, this will cause a lot of requests to the source code provider. + // After Async build is implemented https://github.com/backstage/backstage/issues/3717, + // make sure to limit checking for cache invalidation to once per minute or so. + let preparedDir: string; + let etag: string; + try { + const preparerResponse = await this.preparer.prepare(this.entity, { + etag: buildMetadataStorage.getEtag(), + }); + + preparedDir = preparerResponse.preparedDir; + etag = preparerResponse.etag; + } catch (err) { + if (err instanceof NotModifiedError) { + // No need to prepare anymore since cache is valid. + return; + } + throw new Error(err.message); + } + + this.logger.info( + `TechDocs prepare step completed for entity ${getEntityId(this.entity)}.`, + ); + this.logger.debug(`Prepared files temporarily stored at ${preparedDir}`); + + /** + * Generate + */ this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`); // Create a temporary directory to store the generated files in. @@ -88,7 +124,7 @@ export class DocsBuilder { const outputDir = await fs.mkdtemp( path.join(tmpdirResolvedPath, 'techdocs-tmp-'), ); - + const parsedLocationAnnotation = getLocationForEntity(this.entity); await this.generator.run({ inputDir: preparedDir, outputDir, @@ -96,69 +132,42 @@ export class DocsBuilder { parsedLocationAnnotation, }); + this.logger.debug(`Generated files temporarily stored at ${outputDir}`); + // Remove Prepared directory since it is no longer needed. + // Caveat: Can not remove prepared directory in case of git preparer since the + // local git repository is used to get etag on subsequent requests. + if (this.preparer instanceof UrlPreparer) { + this.logger.debug( + `Removing prepared directory ${preparedDir} since the site has been generated.`, + ); + try { + // Not a blocker hence no need to await this. + fs.remove(preparedDir); + } catch (error) { + this.logger.debug(`Error removing prepared directory ${error.message}`); + } + } + + /** + * Publish + */ + this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`); await this.publisher.publish({ entity: this.entity, directory: outputDir, }); - - // TODO: Remove the generated directory once published. - - if (!this.entity.metadata.uid) { - throw new Error( - 'Trying to build documentation for entity not in service catalog', - ); - } - - new BuildMetadataStorage(this.entity.metadata.uid).storeBuildTimestamp(); - } - - public async docsUpToDate() { - if (!this.entity.metadata.uid) { - throw new Error( - 'Trying to build documentation for entity not in service catalog', - ); - } - - const buildMetadataStorage = new BuildMetadataStorage( - this.entity.metadata.uid, - ); - const { type, target } = getLocationForEntity(this.entity); - - // Unless docs are stored locally - const nonAgeCheckTypes = ['dir', 'file', 'url']; - if (!nonAgeCheckTypes.includes(type)) { - const lastCommit = await getLastCommitTimestamp( - target, - this.config, - this.logger, - ); - const storageTimeStamp = buildMetadataStorage.getTimestamp(); - - // Check if documentation source is newer than what we have - if (storageTimeStamp && storageTimeStamp >= lastCommit) { - this.logger.debug( - `Docs for entity ${getEntityId(this.entity)} is up to date.`, - ); - return true; - } - } - - // Cache downloaded source files for 30 minutes. - // TODO: When urlReader/readTree supports some way to get latest commit timestamp, - // it should be used to invalidate cache. - if (type === 'url') { - const builtAt = buildMetadataStorage.getTimestamp(); - const now = Date.now(); - - if (builtAt > now - 1800000) { - return true; - } - } - this.logger.debug( - `Docs for entity ${getEntityId(this.entity)} was outdated.`, + `Removing generated directory ${outputDir} since the site has been published`, ); - return false; + try { + // Not a blocker hence no need to await this. + fs.remove(outputDir); + } catch (error) { + this.logger.debug(`Error removing generated directory ${error.message}`); + } + + // Store the latest build etag for the entity + new BuildMetadataStorage(this.entity.metadata.uid).setEtag(etag); } } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index fe7b52ab52..ef032e9f8b 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Logger } from 'winston'; -import Router from 'express-promise-router'; -import express from 'express'; -import Knex from 'knex'; -import fetch from 'cross-fetch'; -import { Config } from '@backstage/config'; -import Docker from 'dockerode'; -import { - GeneratorBuilder, - PreparerBuilder, - PublisherBase, - getLocationForEntity, -} from '@backstage/techdocs-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { getEntityNameFromUrlPath } from './helpers'; +import { Config } from '@backstage/config'; +import { + GeneratorBuilder, + getLocationForEntity, + PreparerBuilder, + PublisherBase, +} from '@backstage/techdocs-common'; +import fetch from 'cross-fetch'; +import Docker from 'dockerode'; +import express from 'express'; +import Router from 'express-promise-router'; +import Knex from 'knex'; +import { Logger } from 'winston'; import { DocsBuilder } from '../DocsBuilder'; +import { getEntityNameFromUrlPath } from './helpers'; type RouterOptions = { preparers: PreparerBuilder; @@ -141,13 +141,10 @@ export async function createRouter({ dockerClient, logger, entity, - config, }); switch (publisherType) { case 'local': - if (!(await docsBuilder.docsUpToDate())) { - await docsBuilder.build(); - } + await docsBuilder.build(); break; case 'awsS3': case 'azureBlobStorage': @@ -186,9 +183,11 @@ export async function createRouter({ 'Found pre-generated docs for this entity. Serving them.', ); // TODO: re-trigger build for cache invalidation. - // Compare the date modified of the requested file on storage and compare it against - // the last modified or last commit timestamp in the repository. + // Add build info in techdocs_metadata.json and compare it against + // the etag/commit in the repository. // Without this, docs will not be re-built once they have been generated. + // Although it is unconventional that anyone will face this issue - because + // if you have an external storage, you should be using CI/CD to build and publish docs. } break; default: diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 69fe41dd32..f782dcb3ad 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-techdocs +## 0.5.6 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- 41af18227: Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`. +- 8f3443427: Enhance API calls to support trapping 500 errors from techdocs-backend +- Updated dependencies [77ad0003a] +- Updated dependencies [b51ee6ece] +- Updated dependencies [19d354c78] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] +- Updated dependencies [b51ee6ece] + - @backstage/techdocs-common@0.4.0 + - @backstage/test-utils@0.1.7 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.5.5 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index ad9e1f7bef..afd829d94d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.5.5", + "version": "0.5.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,11 @@ "dependencies": { "@backstage/config": "^0.1.2", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/test-utils": "^0.1.6", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/test-utils": "^0.1.7", "@backstage/theme": "^0.2.3", - "@backstage/techdocs-common": "^0.3.7", + "@backstage/techdocs-common": "^0.4.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -51,8 +51,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 88453e8efb..493fb16a28 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx index 0b4a86c99d..9fb576b200 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -14,39 +14,35 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Content, Header, HeaderTabs, Page } from '@backstage/core'; -import { General } from './General'; +import { Header, Page, TabbedLayout } from '@backstage/core'; +import React from 'react'; import { AuthProviders } from './AuthProviders'; import { FeatureFlags } from './FeatureFlags'; +import { General } from './General'; type Props = { providerSettings?: JSX.Element; }; export const SettingsPage = ({ providerSettings }: Props) => { - const [activeTab, setActiveTab] = useState(0); - const onTabChange = (index: number) => { - setActiveTab(index); - }; - - const tabs = [ - { id: 'general', label: 'General' }, - { id: 'auth-providers', label: 'Authentication Providers' }, - { id: 'feature-flags', label: 'Feature Flags' }, - ]; - - const content = [ - , - , - , - ]; - return ( - - {content[activeTab]} + + + + + + + + + + + + ); }; diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index f3d50dc675..b6744216f3 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/yarn.lock b/yarn.lock index 05282e5285..3bfd57b222 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,182 +102,6 @@ resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d" integrity sha512-T1Ham9sqZKCtSowXRPaBCRy2oz3KHglqqrKiaO7lEudpP6lwH5SwXaq4qliyKzWaqd22srJHE4szdsorbFZKlw== -"@aws-crypto/crc32@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-1.0.0.tgz#6a0164fd92bb365860ba6afb5dfef449701eb8ca" - integrity sha512-wr4EyCv3ZfLH3Sg7FErV6e/cLhpk9rUP/l5322y8PRgpQsItdieaLbtE4aDOR+dxl8U7BG9FIwWXH4TleTDZ9A== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/ie11-detection@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-1.0.0.tgz#d3a6af29ba7f15458f79c41d1cd8cac3925e726a" - integrity sha512-kCKVhCF1oDxFYgQrxXmIrS5oaWulkvRcPz+QBDMsUr2crbF4VGgGT6+uQhSwJFdUAQ2A//Vq+uT83eJrkzFgXA== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/sha256-browser@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-1.0.0.tgz#9c34d3b829d922b2c8e077b30a60db53d6befcb1" - integrity sha512-uSufui4ZktC5lYX6bDxgBgNboxGyw9V9V+rlcNsNTxh4YPhOdCslwJMGntiWOBRGAgXhhvWi7FqnTS2SaT3cpg== - dependencies: - "@aws-crypto/ie11-detection" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-crypto/supports-web-crypto" "^1.0.0" - "@aws-sdk/types" "^1.0.0-rc.1" - "@aws-sdk/util-locate-window" "^1.0.0-rc.1" - "@aws-sdk/util-utf8-browser" "^1.0.0-rc.1" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.0.0.tgz#ca788a3242a4024c386e6b9985da28f290a79ad7" - integrity sha512-89kqtFs/tdHBFHEBXZ4UXlCISswvEor3BVVOriR68Tbk1Qe1zBOZtfbSOt3CDT69z88x5uM558YW9k8I1xei5A== - dependencies: - "@aws-sdk/types" "^1.0.0-rc.1" - "@aws-sdk/util-utf8-browser" "^1.0.0-rc.1" - tslib "^1.11.1" - -"@aws-crypto/supports-web-crypto@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-1.0.0.tgz#c40901bc17ac1e875e248df16a2b47ad8bfd9a93" - integrity sha512-IHLfv+WmVH89EW4n6a5eE8/hUlz6qkWGMn/v4r5ZgzcXdTC5nolii2z3k46y01hWRiC2PPhOdeSLzMUCUMco7g== - dependencies: - tslib "^1.11.1" - -"@aws-sdk/abort-controller@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.1.0.tgz#6b1e6cfe7410ff38ff5efdfc2e16c309d7c4e233" - integrity sha512-rqE/+BBEvF4ZxT+J1hqaSDUzXTcdnyeHQ7vqEyB1UuAmMcq6nIi1rAQKqBXneaIC4WVER3K5f00NKZGLzzqnRg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/chunked-blob-reader-native@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.1.0.tgz#49573fe4087f07894deef7a1bf184517c3fb8f24" - integrity sha512-ghBtZkhUWgy51/651l/GUR/qhdqjFR3GSCsz0B7qisrXc8ZNsd7OlXfnTfYNoySxD3XKpbcxsncytH4Hkxgi4A== - dependencies: - "@aws-sdk/util-base64-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/chunked-blob-reader@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.1.0.tgz#0a17272040760ce54ee6cd5b7e56efaca81e07aa" - integrity sha512-/2fxbKwta8ZiSj59B8F3FyVRszo1/VOhpCeL16gmRRNV73rM3IqJD+xOaDdkc/sFYyBeWn/UhwgD98kxae9XsQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/client-organizations@^3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-organizations/-/client-organizations-3.2.0.tgz#f9fc15ecc61fdf31d1cc1eb5590e526b60bc83c0" - integrity sha512-zIFHW5tETCeoSkmLh227eG25B4HnngVDktXhbPmp1NJjT2pcZzL5QIXuNcPz3PTbkaDNRdB04uRdDqryK4kY/g== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.2.0" - "@aws-sdk/credential-provider-node" "3.1.0" - "@aws-sdk/fetch-http-handler" "3.2.0" - "@aws-sdk/hash-node" "3.1.0" - "@aws-sdk/invalid-dependency" "3.2.0" - "@aws-sdk/middleware-content-length" "3.2.0" - "@aws-sdk/middleware-host-header" "3.2.0" - "@aws-sdk/middleware-logger" "3.2.0" - "@aws-sdk/middleware-retry" "3.2.0" - "@aws-sdk/middleware-serde" "3.2.0" - "@aws-sdk/middleware-signing" "3.2.0" - "@aws-sdk/middleware-stack" "3.1.0" - "@aws-sdk/middleware-user-agent" "3.2.0" - "@aws-sdk/node-config-provider" "3.1.0" - "@aws-sdk/node-http-handler" "3.2.0" - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/smithy-client" "3.2.0" - "@aws-sdk/url-parser-browser" "3.1.0" - "@aws-sdk/url-parser-node" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - "@aws-sdk/util-base64-node" "3.1.0" - "@aws-sdk/util-body-length-browser" "3.1.0" - "@aws-sdk/util-body-length-node" "3.1.0" - "@aws-sdk/util-user-agent-browser" "3.2.0" - "@aws-sdk/util-user-agent-node" "3.2.0" - "@aws-sdk/util-utf8-browser" "3.1.0" - "@aws-sdk/util-utf8-node" "3.1.0" - tslib "^2.0.0" - -"@aws-sdk/client-s3@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1.0.tgz#9c39f4588420696e1a4920f8315f778a7896b858" - integrity sha512-RX/eBi3mHoJGkS146tA4ScSdOwSLEkHWlETFkcqVYdH6IP5FT9CiwRqSf6aNRE+LQiZXoBxeOdvGFXg2jWg83w== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.1.0" - "@aws-sdk/credential-provider-node" "3.1.0" - "@aws-sdk/eventstream-serde-browser" "3.1.0" - "@aws-sdk/eventstream-serde-config-resolver" "3.1.0" - "@aws-sdk/eventstream-serde-node" "3.1.0" - "@aws-sdk/fetch-http-handler" "3.1.0" - "@aws-sdk/hash-blob-browser" "3.1.0" - "@aws-sdk/hash-node" "3.1.0" - "@aws-sdk/hash-stream-node" "3.1.0" - "@aws-sdk/invalid-dependency" "3.1.0" - "@aws-sdk/md5-js" "3.1.0" - "@aws-sdk/middleware-apply-body-checksum" "3.1.0" - "@aws-sdk/middleware-bucket-endpoint" "3.1.0" - "@aws-sdk/middleware-content-length" "3.1.0" - "@aws-sdk/middleware-expect-continue" "3.1.0" - "@aws-sdk/middleware-host-header" "3.1.0" - "@aws-sdk/middleware-location-constraint" "3.1.0" - "@aws-sdk/middleware-logger" "3.1.0" - "@aws-sdk/middleware-retry" "3.1.0" - "@aws-sdk/middleware-sdk-s3" "3.1.0" - "@aws-sdk/middleware-serde" "3.1.0" - "@aws-sdk/middleware-signing" "3.1.0" - "@aws-sdk/middleware-ssec" "3.1.0" - "@aws-sdk/middleware-stack" "3.1.0" - "@aws-sdk/middleware-user-agent" "3.1.0" - "@aws-sdk/node-config-provider" "3.1.0" - "@aws-sdk/node-http-handler" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/smithy-client" "3.1.0" - "@aws-sdk/url-parser-browser" "3.1.0" - "@aws-sdk/url-parser-node" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - "@aws-sdk/util-base64-node" "3.1.0" - "@aws-sdk/util-body-length-browser" "3.1.0" - "@aws-sdk/util-body-length-node" "3.1.0" - "@aws-sdk/util-user-agent-browser" "3.1.0" - "@aws-sdk/util-user-agent-node" "3.1.0" - "@aws-sdk/util-utf8-browser" "3.1.0" - "@aws-sdk/util-utf8-node" "3.1.0" - "@aws-sdk/util-waiter" "3.1.0" - "@aws-sdk/xml-builder" "3.1.0" - fast-xml-parser "^3.16.0" - tslib "^2.0.0" - -"@aws-sdk/config-resolver@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.1.0.tgz#36987002c18884847aa1c96e0daf546b5f9caff6" - integrity sha512-/8hNlmeUPd1Ey9WqaZsydjWpDh1rcZpzOiiIhszOj3gGhUhsGsYGqP5oCwR9vMJf0Z5+o9diGT1QLMy9Rno6pw== - dependencies: - "@aws-sdk/signature-v4" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/config-resolver@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.2.0.tgz#c83de904f155282c91e0972b5053937ea1f05160" - integrity sha512-+PRjXpWq8JlN/ilO2F5sh/HAOkoCJkGnu/e8rfnND88tghVVauFPm7xTR2LHOGp1ugpUJm8lG/iEzpYP1EUcpA== - dependencies: - "@aws-sdk/signature-v4" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-env@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.1.0.tgz#eaa3ed04960faafd4536e9fbb051d5007985ba03" - integrity sha512-WUSrtg/on6bP6rjOPdjhFMkPZgDoFZZL2FST4y9K6TkRxcDHejgPZmP13L74RBGIOYlcflZAxE/936OpzpsAAA== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-env@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.3.0.tgz#7930e504a7a79ab98a9fd34befc5c84b8c4df679" @@ -287,14 +111,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-imds@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.1.0.tgz#33d48753dc00bddce79d2aa8076a7cb5bf8562df" - integrity sha512-I8P0ASEjuYpcDMfU2QKlT8vE3nIo803ct4y5Q54Osh5K7+H8Y+raYK9mxxhjycriDXhcQrOe2Rfj+1ARXjdvGw== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-imds@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.3.0.tgz#ff0cf5489853c16d23fc99d7bae425587e836c40" @@ -304,15 +120,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-ini@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.1.0.tgz#1a6cf9ab9fa1450d4472b9e371099b0c0283349b" - integrity sha512-6kPFcsnCR1tEaVQPJGY6z58XvbeVtjsdC2srYG/0y87zyWd9awGuvRN4OV6aOr24QmEVaFNyWuE5QBQJ1/7hkA== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - "@aws-sdk/shared-ini-file-loader" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-ini@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.3.0.tgz#55fe8f391b72d30e650ba8bc680e82bbeacbbfe5" @@ -323,18 +130,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.1.0.tgz#89bc8803752a3e580c6f2410306c7edad6be7fa2" - integrity sha512-/HNq75cNiXzxm6F+ZeJ3awuqUofUtcr89ZnSnwB1pkicz81yrqAH8EN2dEj90eDo1MdBQHx6rhaKg4SJUaBy3Q== - dependencies: - "@aws-sdk/credential-provider-env" "3.1.0" - "@aws-sdk/credential-provider-imds" "3.1.0" - "@aws-sdk/credential-provider-ini" "3.1.0" - "@aws-sdk/credential-provider-process" "3.1.0" - "@aws-sdk/property-provider" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-node@^3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.3.0.tgz#5c97323fa7b23590070d06aa7b1be8d93b2bf4be" @@ -348,16 +143,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-process@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.1.0.tgz#ff817b29a9760c463b77be3ce49375eaeb753ef3" - integrity sha512-pecJk5W9LYLk9KOwT5A3X6ECHoakQMvXL/whqDsKdXQ4c5C1pVoV67Jnp3ilMNfr6CauxU8gQs53iPS5LUHH4A== - dependencies: - "@aws-sdk/credential-provider-ini" "3.1.0" - "@aws-sdk/property-provider" "3.1.0" - "@aws-sdk/shared-ini-file-loader" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-process@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.3.0.tgz#9de0984bd6dd0f5e40cff3672d7dd19e8cd43074" @@ -369,338 +154,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/eventstream-marshaller@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.1.0.tgz#65a217e37abcaa162276ccb1d4487d42431d1534" - integrity sha512-ZfWK+QPB+nuKfd90ZWpkJtK4wSKYv5qzA59jP1wwEWZ2XzUyTs2FWq4rns4af2UHnd+r5+92V2r/AZUCzT9U8w== - dependencies: - "@aws-crypto/crc32" "^1.0.0" - "@aws-sdk/util-hex-encoding" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.1.0.tgz#e62f745ecb5336a957fe645f6d596501051ab73a" - integrity sha512-a7P8JzJhHTFAc6sY2uBVMr8RpLap58LXOgWUEQmozFOK6Klu2NxsdzfNGJAURsc1tK6cw126fidA9UMH/NR20Q== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.1.0" - "@aws-sdk/eventstream-serde-universal" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-config-resolver@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.1.0.tgz#f4f95c4770ea34b06d79ef61eb8cbd73ac8403bc" - integrity sha512-OxgQNKNKOuXAy4ID77EC7glHALGOGdDt+fDteeajEEZ+XXQ2q4maIAbQ8N44bZcy4E8D20YEesaHDAtsDiUvPA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.1.0.tgz#23d6630385d7023d3da9c905174ee391b5d9e5d6" - integrity sha512-xn3SwHhi9UDi6gE9fQxD8j+fxRzfnMWiu688sneoGSSzZl4e5lZtrRbMGWKdtT840dp+GrEXE6BVobUelOyVHA== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.1.0" - "@aws-sdk/eventstream-serde-universal" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-universal@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.1.0.tgz#c48f51c92711cf0ec3a8e93b466d4dde8128d936" - integrity sha512-dnrsnLqBnFddxLtFGzDE9SBNl7+xJmgsL7SI9ALqxgvJkK7MVVj9+kzCEyypNHcRCG6OUTypvY+wjgMXJAtHUg== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/fetch-http-handler@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.1.0.tgz#2e495cbd5a633c3a5f9935b59faa9c8c0ed8b5e2" - integrity sha512-QCYGCdQuV3XLgqvKoJD3C/GO+cEZQOazII/rPfaeVXquv8Xrn85UJ3PZ3uFCzVVmjnm1RidwseRQoYrSsjmRhQ== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/querystring-builder" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/fetch-http-handler@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.2.0.tgz#116bfc06b3de797d4c746962874ef00e66fda024" - integrity sha512-Y8cw7+HNhxSpYZmd78Bq46rYUUCqnRH9AgHXkLifsigt4RsEAfYhODhEUQhEhJ/zwgadvj/fIKJWKdSiCbiC1A== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/querystring-builder" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/hash-blob-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.1.0.tgz#5c6548b05d155d3141b47da958be13043a96a17f" - integrity sha512-A/iLk2P/15G//YPlB9DwNPR3J3QlrmJSrHKKYLj/t1DnxiIu3DZNCZjhLmBuQpx14O1GcolmewqJRaAq1qtVcw== - dependencies: - "@aws-sdk/chunked-blob-reader" "3.1.0" - "@aws-sdk/chunked-blob-reader-native" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/hash-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.1.0.tgz#2d9eb6c0a5b65c179d2e60db941ba921a83e1861" - integrity sha512-zcXjzPuraq6EEEC0N2DI/ng554XiHtLsWGYK2TkW5yS6+2xXiJFny3V5wZHWXsidHOgwgML92r/pU0PeVcTCbg== - dependencies: - "@aws-sdk/util-buffer-from" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/hash-stream-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.1.0.tgz#05424ef3873be626091b55617fe1df135ee34dcd" - integrity sha512-GQ7EsdH9yIYIVPhtcuwpYaTS3MNq+3jo6mUF06m8YxlG58GNIVz9gG8oMy9ezP5rpIjJX9YhXKN8GgERNxfpvQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/invalid-dependency@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.1.0.tgz#834c4e4a34d481bd3d4467437441ab035634710d" - integrity sha512-PNKFfIC9w25MA/fV5FnjFfFpbNiLaojgS2RtT/+L5pyic2NJ3spGNlF0pMrnPPtbrFFHaOy/E7bRa0qVtNWEyw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/invalid-dependency@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.2.0.tgz#2c2c19cbec17ff28333c68e28420db6902a4374b" - integrity sha512-8hlqE4pWo2DoM6pAqjL+A3zYwUMaxP/RCGO+0zKbZHVw2stdRDzpO+jUfzZHR+mPJ+L/2wPwhK8D9n3yBriNBQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/is-array-buffer@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.1.0.tgz#7ac296d6408e34083ac007630541a1cdf67387fd" - integrity sha512-wE6Am+/FKuINc/aypXiBiLAatlSyxYQ9wGGQHf2iYOX5d5bHLOVKPoRwcqSCaiaR32aRcS7R+IhgxeBy+ajsMQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/md5-js@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.1.0.tgz#ae0799f9d9eeab01f4dd72baaa6bbb63cd45ffed" - integrity sha512-QB8D7EdxjOih9NWL/NoJRkgE0RSSJTXQLuQGH9UMj66rUQfGpwvLJDL+PbvoSUJgM8+McEBGyvjLCrDCK5r+qA== - dependencies: - "@aws-sdk/util-utf8-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-apply-body-checksum@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-3.1.0.tgz#85504e7e7883830ffa06954a07f57b1fdb003408" - integrity sha512-q1Jx8HhC7fQjfrCUjbdlRw5F0Vr+gwjHf4sKd+S+FYO+15+PVtukBiKfP3lEW4KKREjTv1h/qPEjhv+CK4nN7w== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-bucket-endpoint@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.1.0.tgz#38fb980fe99c42ddc264db40b93f28f84b3cc1e2" - integrity sha512-/NZuSbqSNARgsZqrRZ8yDXGqF0MSdA/vhNwXAk8BVz/oav4fveJ4KiMMO7Pq5918ZWJ3v//4bSezTnKLDzKmAw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/util-arn-parser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-content-length@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.1.0.tgz#af100e6f724a188bb2963372de8d40868905be51" - integrity sha512-+QGIYkAch0q3a8LrbCS21MEKlE0WgtZBOJl76M3LZtvnbvgKkAmL4oDc+HjGD1jYuNRSt+hQZgqofuBDe8fyyw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-content-length@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.2.0.tgz#876943c7498e930595621395e5928fc59cf8ed68" - integrity sha512-xS7zdcRcubv7WKs5L/RG69EuuatuPxYNQT4O9epQyUaOpTHKzdVd1wnBK+otWYxI9qnCjIXwwmMyGbJXD9OHhw== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-expect-continue@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.1.0.tgz#3453aeb345ea852ebeefe392f7706b7573dd3c23" - integrity sha512-NCuF3BMf5QqXv4iYr4eGnmg7BfIpFjPvjxapB5cQbzUOXIyUtlYWUu7QrsdBxCsqgOhVJ4mAiqliVsdxML5yZA== - dependencies: - "@aws-sdk/middleware-header-default" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-header-default@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-header-default/-/middleware-header-default-3.1.0.tgz#0b41d113f878d95132e10a48ca4c9667c28cf325" - integrity sha512-BJFQXxkkuIkN679bqqTo9hBUuTFtO4izzSY3WPnnYYcQwYTIH/4JSifCpJWw+AjbvTZ2rA2DrWaTNGz3zuiBGA== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-host-header@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.1.0.tgz#61f22f7d7ce8907d0dbc817ff67ec5545012dc51" - integrity sha512-x86fAcqhtK6ObgPJHFLRaZP+cC4gI185QwxqP72balLgIsJJ6InS7441sOMPr5AZ9xgLJyPPKXouQ5zFBYjwpw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-host-header@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.2.0.tgz#cf5d0868f939834fb45c4490c1118eaf25317505" - integrity sha512-URRRaqO74DSdhqYyKbyCyadJ9No17KBMmgwz2OKecdwp6/pUVNQmlxhj1cG9leWUkBuOHo6weRvWc5zXwvfy1Q== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-location-constraint@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.1.0.tgz#181007815ca9167b333666eb073b131e17d86fe7" - integrity sha512-NR8us/ZVJlxg/10h+dyu6xnKofSBWTp+Ai4yvkEFBsxr9kruVqGMP/2IFXl6iZAY5uy/H+TUJzyYtd4/DUgNgg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-logger@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.1.0.tgz#6356f6d79524493a0485bfd86f621d7a882daa1a" - integrity sha512-RYJbms7ECg1FgYmN/IyK9U9nzWZtUmt2ZPBunUqvab/ldjaXpAtJq3IYdl1E/rgbc0LSTRDjyGC4erMDOT8IJg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-logger@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.2.0.tgz#9d4bf84fbf7a0ddc144e6c660f1b806a23a42a6e" - integrity sha512-vkex6bsYqafdvrLe4MXJDJrEfvRb5Z8z01lVlaEV2bzXFqw7WNuFVDRC80GE/YB+r2ds/n9QU4EYUSX9wewoJQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-retry@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.1.0.tgz#4df049c73121a83e19f5078e90a411b93a8cf7de" - integrity sha512-jNewsmLhuSHCtwpuQlsOuQ/Cig7BE4BIF7cfpWaczqYaG1F/rdTlAmb69ugg5pVby7ccj2jgA4aw+achjuzqSw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/service-error-classification" "3.1.0" - react-native-get-random-values "^1.4.0" - tslib "^1.8.0" - uuid "^3.0.0" - -"@aws-sdk/middleware-retry@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.2.0.tgz#72bf988d90d9b0192aa7b290fc340b9f2e57a193" - integrity sha512-roecjUD9di1kqyCewk8pGXcsiHyi8kte5Is5bo9kCPfqHhtDN9L/efOhC+rhT0N9nKplC4rZGE4aopxNhMbLtQ== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/service-error-classification" "3.2.0" - react-native-get-random-values "^1.4.0" - tslib "^1.8.0" - uuid "^3.0.0" - -"@aws-sdk/middleware-sdk-s3@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.1.0.tgz#e602bde5b9b5abe33e9087f61cf0f0cae9157bad" - integrity sha512-qH2RBt9EdDIO/2PGl7/QyIt9C4WuY6kVfx/S1Bh26wSY9s11VU73CDanZtgNQwIiX20v8DaPlg5dp2hse7Nr7w== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/util-arn-parser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-serde@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.1.0.tgz#a80b96dc0008546d9e9e9bce7e9b1ec134edbc6d" - integrity sha512-vXdYlzeBoJCqe+xhlFE9J62EyREvU1MVC6p3m3QPtfXuYlQwRb8WhlFFDFrDdA7V2usBjx5aM67OZnapplyUfg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-serde@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.2.0.tgz#aff46b0457f44738cdc542ea4bea01160d437300" - integrity sha512-dWTL5lIjosKCUDyaNtEolaGpo/KwoM1Zx4NdMUBLYBy9v0vKSnpRLPXxyM2EREVJYHTVrvm6UoW8dM+5RFiN7A== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-signing@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.1.0.tgz#d48c18f159f867ca5efe34c4993880e6820cc11e" - integrity sha512-3zNsonJ/nkBL8gUWr2v1vQ/NKIQQgSjmrxIa4xUxRcqOOO1qgZgjablxYHQlX07B6defhaOS2/p/EDmF+LfPqQ== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/signature-v4" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-signing@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.2.0.tgz#2ad5288d6543281a7e704102f6d446acdb9efaaa" - integrity sha512-XkDu0lMkdlf95nE2aYyAFmXOzBD+noVebyB1dlJDQCZWNzGkefc+8Nez3lCgyd2+KuEyHXMmQsqZwnmygpRvLQ== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/signature-v4" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-ssec@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.1.0.tgz#af57795facc29673568277d1d9aa090e57d69669" - integrity sha512-RGHWxNEMyYHv3et23XjMVS+9HhJIPR+oO21h82Eur4WhA6u3EuiZ+TKuoXZ9DA5V2wVJvPg33NHi0/uu1mah5w== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-stack@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.1.0.tgz#31a69784ff31d70e1176d948f94294563b23b36e" - integrity sha512-lin0C0xPspT/orPMWWHMYG/7Z128NsSj6Khs4G6TH+2rIixXxQtHLen8H2dSPNIYXnLaxvtUDl5VuqjRt+s2Ow== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-user-agent@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.1.0.tgz#b35ce7fae8f672aa9922bc28082137fa8c2f0486" - integrity sha512-ghUD5ZhGdvPEctacO+INnQpdpQUV8N9hC9We7ftAE75PGBG/aB8qnVWYV8uKAxx8ruNAi+iU4j+4B4qLFNMx2A== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-user-agent@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.2.0.tgz#19e64961feb6a41721e3164538031e21858ad7ee" - integrity sha512-rME49AFlVZSwgxTLQo/5Ok012HRcg9xhSFK4oOEkPJiYyGedvsF+Qy6sz3VjQRL2SvUvoAp03Wqbxe/Keewdbw== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/node-config-provider@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.1.0.tgz#74b37f65faa90fa6b4acb640e0f215f50aaa4904" - integrity sha512-zbV5Va40elco6IBddbDmhMQDWazQxHAk4trWFj3MrH36rpWZzWwsRbgjgGv0CcvSy5zP6PLHjdcBPeHS/KmViQ== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - "@aws-sdk/shared-ini-file-loader" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/node-http-handler@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.1.0.tgz#b7fdbc3a1928127b2eb2064177502e11324f7bb8" - integrity sha512-T1yLundJ4tPeIt9IaOk24kBNBCe54UhCXXrh/0SSZZ26DLINPV5mF22wZKxgMpvKTbtJ6vlag+DhJe/ldqJ2qQ== - dependencies: - "@aws-sdk/abort-controller" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/querystring-builder" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/node-http-handler@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.2.0.tgz#cf6b12b021d61933202522179e5bd5710e565afa" - integrity sha512-C6bwM9RNINF2r7wTjQ7esjwlTnEIUZkJ+hyMGwOUxc4AqHCdc1PGEFwuMjQWhiyUtvR3hd7hfPj5GWvFP/PUTQ== - dependencies: - "@aws-sdk/abort-controller" "3.1.0" - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/querystring-builder" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/property-provider@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.1.0.tgz#19cf1da802a09be429e26f8eafec9b4c3473f8b0" - integrity sha512-NDCI7T8ytbzlXW8axDdtjMhJYRMVbOoPupM+HwR81hT7QoWWNrpCrjFqIv/gJet+ms5i4k80CWrBJd0L0kNl/Q== - dependencies: - tslib "^1.8.0" - "@aws-sdk/property-provider@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.3.0.tgz#49979cb1a3e5562d51807c7403c5fd48cb9f2cdc" @@ -709,45 +162,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/protocol-http@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.1.0.tgz#7da0ebcf02a40a8300f3bd52f9206f25fdf1ca7f" - integrity sha512-61qInY/AESslV6ZYTAgwoB172K/H+5EiXeWnmWExOGH3vkfkkxQBYCTcATdtasP6QYTfYiePhyjJ8eUyQL3C0w== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/protocol-http@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.2.0.tgz#0d352045bae38c66a2c97611983bfb545351c4e0" - integrity sha512-7FA23ABTZ18MsQD0GcICj6dU9HRInf6l6XvtnlPUS6ZvjDbsNRqEROUtvjl8dkDDP1mFpEQuHJ5e9MO5peEm7g== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/querystring-builder@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.1.0.tgz#6cb859cfd906faa21de32641a960f080be78f0f3" - integrity sha512-rfVJWrAT+PJ5Dx0arxK5RBMfETezAjKGHxXWf7/9rcxSqoEz8s3tYhZ7/CqcOQqN89X6gJ4veQwGprraIhOZIg== - dependencies: - "@aws-sdk/util-uri-escape" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/querystring-parser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.1.0.tgz#eec9fcbfc1c907e19909b4436f93a6c9fb063041" - integrity sha512-tqwHKLKplyv940+cR8uW6w2K3IEcGOskJ/8oP1R3RSyyidONSVEShNlpApTQlccfYriieDuEDLVCo4pT7S55kw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/service-error-classification@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.1.0.tgz#9471f761f36ebe3efeb0791d7e2af517f87ef4a2" - integrity sha512-zUNV9Fyguto8VOhinKvzIoQxwfYMSSLO6xTKJLyTB+cDv51SX5sh+lqX6IKGhuqz2Wse1ynuoLGOnmVoe4aUpQ== - -"@aws-sdk/service-error-classification@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.2.0.tgz#286ba27368276132db89f21b79724bf95cbd8d97" - integrity sha512-ZzeSZ0HnJq5zUkA466zVEZAE1SVqYuvGzxRGTEszbMW2eWPLonJgUwiT5nDScmJQcDju044iP//2KnBvn62Rhg== - "@aws-sdk/shared-ini-file-loader@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.1.0.tgz#b1bc7ed4e16535f20c788915060a121e457efdfb" @@ -755,205 +169,11 @@ dependencies: tslib "^1.8.0" -"@aws-sdk/signature-v4@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.1.0.tgz#b5d480af74629e86163ffe86a3f13a37f1f1b250" - integrity sha512-TFbe/Xf02a4baU1tg/S/w1A5AKaD7YHuqQg4Ak+HbRvAFe77/C/rbqWGnl66BDcukMvx13Ywd5ZkraAVSaTlzg== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - "@aws-sdk/util-hex-encoding" "3.1.0" - "@aws-sdk/util-uri-escape" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/signature-v4@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.2.0.tgz#9eb5c4c3f5c3d448f3929f310baa4ad3682d1ddc" - integrity sha512-Fb81gqaSnuCwO9HFYnztksyFud5X6/Ikr95X3pfrtLcHlHe/S079woWmlDUEgy7lw2X2GY94TcX9jlXuMhTfhA== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - "@aws-sdk/util-hex-encoding" "3.1.0" - "@aws-sdk/util-uri-escape" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/smithy-client@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.1.0.tgz#dc60223b9af1e99d076871e8138ea9b6ecbf737b" - integrity sha512-l/xCyQ0xLvIX/VZDYC3CGAphXtV7/t1gqfxfATbx1TD3E23PS0XvaV8JAmMWF+Q+rShk+otuYpdaRjGrygTK5w== - dependencies: - "@aws-sdk/middleware-stack" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/smithy-client@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.2.0.tgz#ba93d125f83f385ce6a4600e47339bf036fc7d0a" - integrity sha512-LpBGQ5m/oQ6QkMPQl7yxvBll/NRww4GUCEwpEXw412SUbX7aVTu+CNoUmF2lg8Gwp2XM3NgOkJ9DQ94YwZ/dug== - dependencies: - "@aws-sdk/middleware-stack" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/types@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.1.0.tgz#04d77c37a80b422e8123f296338d129e51f3e1fc" integrity sha512-4Az7cemXCN4Qp8EheNkZTJJqIG0dvCT2KAreJLoclcVTcEFw2rzlATUnSeia1YTRsVd6aNxD001Ug7f3vYcQkw== -"@aws-sdk/types@^1.0.0-rc.1": - version "1.0.0-rc.10" - resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-1.0.0-rc.10.tgz#729127fbfac5da1a3368ffe6ec2e90acc9ad69c3" - integrity sha512-9gwhYnkTNuYZ+etCtM4T8gjpZ0SWSXbzQxY34UjSS+dt3C/UnbX0J22tMahp/9Z1yCa9pihtXrkD+nO2xn7nVQ== - -"@aws-sdk/url-parser-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/url-parser-browser/-/url-parser-browser-3.1.0.tgz#8e657d9e6cbde454df3b6e597e4fa4b690fe47af" - integrity sha512-XtiorfIxhbJuU1TLp7rL7qMzNY/+9FYisFtCFcrcy7/fsvXUTjjB2u3O4J01m/T6/HVmenV+SFQvs+fYC9U7dA== - dependencies: - "@aws-sdk/querystring-parser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/url-parser-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/url-parser-node/-/url-parser-node-3.1.0.tgz#f49690405195f80157997fd48f99e1e036de3a3f" - integrity sha512-pDQekV4RK2hoiDB4YkMi00oTgU8MoHg+Sve9HaLYCtTuirHMVBswxzGg6MVDWo0tM6WSFbnQRXNZtioUo7ExVA== - dependencies: - "@aws-sdk/querystring-parser" "3.1.0" - tslib "^1.8.0" - url "^0.11.0" - -"@aws-sdk/util-arn-parser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.1.0.tgz#56c462b2ef6d7cfa098e1a22e44fb3b52da0718c" - integrity sha512-xXL/nadq5mqEw6Mrv1ghoODuyWWsAxvr+rRNgDJOav6mypgEOiLb0ybkqinrH1ogTkAYbegs+uaWxgSPBe9ZSA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-base64-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.1.0.tgz#756253a3fc1ad58c38ad28ed664b701d850d3aa9" - integrity sha512-xkodj0VnkHl1gdYI9Nl4E2Ed+atM3xBTNaedoGnmqoyosMjPRJCpU8uFBmdiF4e+GGPsXlYe9oA/hLyJFxmeSQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-base64-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.1.0.tgz#027a9e854d204adda56d5f43f8ef4a20532b7ac9" - integrity sha512-FEtnINw2MeD3LAtyGcofah5D8j6OjpmwNKibr7mIgosRO++iVyXe2xa6iOoptZFn5pIU0C4fkJn5o+kjBhRafA== - dependencies: - "@aws-sdk/util-buffer-from" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-body-length-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.1.0.tgz#b68018860cd5d63c9327c7a42323c5c75cf514bf" - integrity sha512-vzKDD/p1gcA05jeLmn6+6HdOY4G6Axyp6dj1R1nVeFpPPx6KkFsNGL9/CoaRT2TGv1fHBoDXsve9JRaCxrER4Q== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-body-length-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.1.0.tgz#c280223066f0ce8fadb002a668a5f5e6ee4de12b" - integrity sha512-MfJoU2wFWkOmbjWDepq5bDGYZlpvtBi2Vs8ZeTcm/4+q+3L9tJ/Zb/Ofx5oeRg9VhCsAjvceQTdX+CAyP8byXA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-buffer-from@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.1.0.tgz#a68b2e191f3737bcd85c33af5a6b0e3e9f974f0e" - integrity sha512-UeC4VKmWYgTXjNdLVHfurrdhznnoxWLUFx8xspyRd58BhSZ5vc5HiiKTPX/CGxzAP/qZG668PaoOJucwmEam4g== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-hex-encoding@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.1.0.tgz#937440d60333c1b3e4fbe06012dfdc65c9e297bc" - integrity sha512-MPOsUY3USCUBaqZ3ifgE9il/liVxEKsz6dYQ08pdtWRzZx2CT7kWslQeNAT565pMvktnvdLjfzBw2FwnSI6nqg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-locate-window@^1.0.0-rc.1": - version "1.0.0-rc.8" - resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-1.0.0-rc.8.tgz#d28175aeb9c8ad3940242e615b1503632d3be33d" - integrity sha512-TvqeA4fgmZ0A0x3K+qVj/OSWEFHGZjzpVuyXlm1EYOf7NQ9VWRlokEn1MYKuL+t7al9ZeQyi16D8Dn7DW1eidw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-uri-escape@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.1.0.tgz#1e4450c8e047b542f87172407e2fe0ade7c55227" - integrity sha512-1ZcXVJpsA6uW3tDTQI+Rpawqh76fyHpFc55ST8VGyMgmCzlJzBpYG0ck1kqVRSUP7YyvkJQvHfcm+U6doL5Xkw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.1.0.tgz#471dec78305f39ac9a05968970917b5c66e4312b" - integrity sha512-w31WYjRTXbBn9z/sRy/4IyjIyEeTOon1JvRzlKDbEWm4JXarVbiJcZKs1U3q+9fS9la5uLM1NEQCsgdI1iYERA== - dependencies: - "@aws-sdk/types" "3.1.0" - bowser "^2.11.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-browser@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.2.0.tgz#f6fdee45817d80a637d7bd66985f516182b6f550" - integrity sha512-ugkLsntS2wpLWCjPs49wnAGhuIAFyHYzTznp0v+Qj/uOkm0ddq4kWp+FwtMxeQMRY0eoBdfqXJAbJLz8w4xlUQ== - dependencies: - bowser "^2.11.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.1.0.tgz#3fa66564a5fdb0e2851554688dc507127356f941" - integrity sha512-VyyZUR4vrHyT4aLH+ufoaTxFy65K9OtoCEdH1X748HinLZY9JObCCO6lZVNr5b26fGJrbqzoAqaXWgHkmEUpoQ== - dependencies: - "@aws-sdk/node-config-provider" "3.1.0" - "@aws-sdk/types" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-node@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.2.0.tgz#9190f69afdd81bacde16e253af2545a0e3f443cd" - integrity sha512-Ovyq6yc7SL0NWZWBrPKRc0D38xkMH2ulslcnukdNOitv0s8V2Ge09R4T63U0I8IJ7nrKa75Uus6A8vC8Zko3yg== - dependencies: - "@aws-sdk/node-config-provider" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.1.0.tgz#7be17b545af101c320d34aace47139cf9987d796" - integrity sha512-vJP20me+Wc1RJHq+Y+gFD25aWhbQte+Qkyh3SOKQ+YvNaMcaeVwOV7b3Y3ItBuMdutHLJWmbJ2wF6dhhpy1kOA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@^1.0.0-rc.1": - version "1.0.0-rc.8" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-1.0.0-rc.8.tgz#bf1f1cfed8c024f43a7c43b643fdf2b4523b5973" - integrity sha512-clncPMJ23rxCIkZ9LoUC8SowwZGxWyN2TwRb0XvW/Cv9EavkRgRCOrCpneGyC326lqtMKx36onnpaSRHxErUYw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-utf8-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.1.0.tgz#fd7e506b5fdf404fbbdd9bc46429afeea8114b25" - integrity sha512-lrBLkROMh9kTjHOguusqLvTX5+5O5CVpAGeISZlW6CCx2pMHtVRyE9cdNuRI8aJpyZsU12j8SoaKDUPGD+ixzw== - dependencies: - "@aws-sdk/util-buffer-from" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-waiter@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.1.0.tgz#e01dbd6bc903e03101152f65ab9d2e0613f08c2f" - integrity sha512-nMaE3aGCZGVQw10IUXBxIbCXkyaX8T1Fn7GvJxeFJflKcXuKFKBfvkhzimSe/hJDC6Ykn7gNuI+EY+ZoCVXqtQ== - dependencies: - "@aws-sdk/abort-controller" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/xml-builder@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.1.0.tgz#ad7113075416436d8c822199674c45fc6ef42441" - integrity sha512-F6liCbWPMbnJq8d0qgzuXwG5O7jg1hhgiG71TTn83rnc6vFzyw2o0C+ztiqSZsbAq7r2PlEfBPWVD32gTFIXXw== - dependencies: - tslib "^1.8.0" - "@azure/abort-controller@^1.0.0": version "1.0.2" resolved "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz#822405c966b2aec16fb62c1b19d37eaccf231995" @@ -2546,7 +1766,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.5", "@babel/types@^7.9.6": +"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.6": version "7.11.5" resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== @@ -2607,7 +1827,7 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.6.0" + version "0.6.1" dependencies: "@backstage/config" "^0.1.2" "@backstage/core-api" "^0.2.8" @@ -2646,13 +1866,13 @@ zen-observable "^0.8.15" "@backstage/plugin-catalog@^0.2.0": - version "0.3.0" + version "0.3.1" dependencies: - "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-client" "^0.3.6" "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.0" - "@backstage/plugin-catalog-react" "^0.0.2" - "@backstage/plugin-scaffolder" "^0.4.2" + "@backstage/core" "^0.6.1" + "@backstage/plugin-catalog-react" "^0.0.3" + "@backstage/plugin-scaffolder" "^0.5.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -2660,7 +1880,6 @@ "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" - moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -2670,13 +1889,13 @@ swr "^0.3.0" "@backstage/plugin-catalog@^0.2.1": - version "0.3.0" + version "0.3.1" dependencies: - "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-client" "^0.3.6" "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.0" - "@backstage/plugin-catalog-react" "^0.0.2" - "@backstage/plugin-scaffolder" "^0.4.2" + "@backstage/core" "^0.6.1" + "@backstage/plugin-catalog-react" "^0.0.3" + "@backstage/plugin-scaffolder" "^0.5.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -2684,7 +1903,6 @@ "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" - moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -3340,30 +2558,30 @@ tslib "~2.0.1" "@graphql-codegen/typescript@^1.17.7", "@graphql-codegen/typescript@^1.18.1": - version "1.18.1" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.18.1.tgz#3d730472a01f18aea6331046f4ebfe3d91326801" - integrity sha512-Ee37NutKmaNrgAo2d5mv42RqPd8jJ6zyUKAH669Gbv0dFn2EK3sdC9PYQC9gXptv+H/eQn2gYgaa2nVpEPAIzg== + version "1.20.2" + resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.20.2.tgz#223327a8aea929e772f481e264c5aa3a36707fcb" + integrity sha512-D/DMUz4O2UEoFucUVu2K2xoaMPAn68BwYGnCAKnSDqtFKsOEqmTjHFwcgyEnpucQ5xFeG0pKGMb1SlS2Go9J8Q== dependencies: "@graphql-codegen/plugin-helpers" "^1.18.2" - "@graphql-codegen/visitor-plugin-common" "^1.17.20" + "@graphql-codegen/visitor-plugin-common" "^1.18.2" auto-bind "~4.0.0" - tslib "~2.0.1" + tslib "~2.1.0" -"@graphql-codegen/visitor-plugin-common@^1.17.20": - version "1.17.20" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.17.20.tgz#cff95cdd49cef270b3811fdb141a412ffe2bdfd7" - integrity sha512-buIpcNNyTqVubknancX8m9jARCZsUA5eKuskg+CylWKL/8CSaD2Tiq7CfbbNO10o7XIgRrPtJMl1c9hQ6N4ytw== +"@graphql-codegen/visitor-plugin-common@^1.17.20", "@graphql-codegen/visitor-plugin-common@^1.18.2": + version "1.18.2" + resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.18.2.tgz#1a30bd33f011b4fb976e0f8462d160126db875ea" + integrity sha512-A8yBJGq7A7gxaVVXK4QXwV1ZpzZ64fH7U7JTGeq86o3jA7QNV2rmCRXCY0JttS2fu+olV18NjsWRwAXuAb1g9A== dependencies: "@graphql-codegen/plugin-helpers" "^1.18.2" "@graphql-tools/optimize" "^1.0.1" "@graphql-tools/relay-operation-optimizer" "^6" array.prototype.flatmap "^1.2.4" auto-bind "~4.0.0" - dependency-graph "^0.9.0" + dependency-graph "^0.10.0" graphql-tag "^2.11.0" parse-filepath "^1.0.2" pascal-case "^3.1.1" - tslib "~2.0.1" + tslib "~2.1.0" "@graphql-modules/core@^0.7.17": version "0.7.17" @@ -5405,16 +4623,16 @@ integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw== "@storybook/addon-actions@^6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.11.tgz#73e91cc95c45ea477cfd4f3603f6b95f5829eab6" - integrity sha512-J44XLx2G732OG7Az79Cpk5UlI5SyXHeQqdykwT/4IEQXSBXAYWSTIJJjpJdcjR/D+zpklab1QDSiWxCrKbe81A== + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.17.tgz#9d32336284738cefa69b99acafa4b132d5533600" + integrity sha512-4hyAvmjnI4C1ZQ7/t21jKKXE0jO1zAk310BkYin0NJf77Qi0tUE1DNOwirJY/xzRih36wWi1V79c/ZOJNsLv9Q== dependencies: - "@storybook/addons" "6.1.11" - "@storybook/api" "6.1.11" - "@storybook/client-api" "6.1.11" - "@storybook/components" "6.1.11" - "@storybook/core-events" "6.1.11" - "@storybook/theming" "6.1.11" + "@storybook/addons" "6.1.17" + "@storybook/api" "6.1.17" + "@storybook/client-api" "6.1.17" + "@storybook/components" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/theming" "6.1.17" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -5480,7 +4698,7 @@ global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.15", "@storybook/addons@^6.1.11": +"@storybook/addons@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.15.tgz#09eb8d962f58bd20b4ac2f83b515831c83226352" integrity sha512-ENyHapLFOG93VaoQXPX8O3IWjLRyVBox9C9P20LMruKX/SfXAXx20qsoAWKKPGssopyOin17aoQX9pj+lFmCZQ== @@ -5495,6 +4713,21 @@ global "^4.3.2" regenerator-runtime "^0.13.7" +"@storybook/addons@6.1.17", "@storybook/addons@^6.1.11": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.17.tgz#ab0666446acb9fc19c94d7204dc9aafdefb6c7c2" + integrity sha512-3upDPJPzUkls2V3Fozzg+JOcv138bF90pbdRe9YSNu37QvRIL+iQODY7oFygMl+kqjG2F1FGw5EvxAV1mnlwCw== + dependencies: + "@storybook/api" "6.1.17" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/router" "6.1.17" + "@storybook/theming" "6.1.17" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.7" + "@storybook/api@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.11.tgz#1e0b798203df823ac21184386258cf8b5f17f440" @@ -5545,18 +4778,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.11.tgz#62c1079f04870dd27925bd538a2020e7380daa2e" - integrity sha512-voW4Z2SUacDOxwN2q1NEBL//8OpgvL2C5CeoG1VQyEllKM8Vg9t1Nxo2FFTJBzv5LeEX7VIJKeBoB25DYvKyng== +"@storybook/api@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.17.tgz#50393ce9b718063b67680212df895eceacc0c11d" + integrity sha512-sthcfuk2EQ3F5R620PBqpI4Pno3g7KQm6YPZA0DXB+LD/z61xH9ToE1gTLF4nzlE6HwghwkXOZyRwDowRdG+7A== dependencies: - "@storybook/channels" "6.1.11" - "@storybook/client-logger" "6.1.11" - "@storybook/core-events" "6.1.11" + "@reach/router" "^1.3.3" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.1.17" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.1.17" + "@types/reach__router" "^1.3.7" core-js "^3.0.1" + fast-deep-equal "^3.1.1" global "^4.3.2" - qs "^6.6.0" + lodash "^4.17.15" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + store2 "^2.7.1" telejson "^5.0.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" "@storybook/channel-postmessage@6.1.15": version "6.1.15" @@ -5571,6 +4816,19 @@ qs "^6.6.0" telejson "^5.0.2" +"@storybook/channel-postmessage@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.17.tgz#309ce67c94637ec13319d4ce360a8f3742ddbaf4" + integrity sha512-2nVqxq4oZdSITqhFOnkh1rmDMjCwHuobnK5Fp3l7ftCkbmiZHMheKK9Tz4Rb803dhXvcGYs0zRS8NjKyxlOLsA== + dependencies: + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + core-js "^3.0.1" + global "^4.3.2" + qs "^6.6.0" + telejson "^5.0.2" + "@storybook/channels@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.11.tgz#a93a83746ad78dd40e1c056029f6d93b17bb66bc" @@ -5589,27 +4847,12 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.11.tgz#d25aac484ca84a1acb01d450e756a62408f00c1a" - integrity sha512-DodJQzGCR+PYs26klvbquTjfBgkw5nvCZd3jpgWQtOrYaY/cMY1LLkVkKqrm2ENW8f7vf7tiw78RtxaXy7xeIQ== +"@storybook/channels@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.17.tgz#2cc89a6b9727d19c24b15fa3cb15569b469db864" + integrity sha512-MUdj0eKr/AbxevHTSXX7AsgxAz6e5O4ZxoYX5G8ggoqSXrWzws6zRFmUmmTdjpIvVmP2M1Kh4SYFAKcS/AGw9w== dependencies: - "@storybook/addons" "6.1.11" - "@storybook/channel-postmessage" "6.1.11" - "@storybook/channels" "6.1.11" - "@storybook/client-logger" "6.1.11" - "@storybook/core-events" "6.1.11" - "@storybook/csf" "0.0.1" - "@types/qs" "^6.9.0" - "@types/webpack-env" "^1.15.3" core-js "^3.0.1" - global "^4.3.2" - lodash "^4.17.15" - memoizerific "^1.11.3" - qs "^6.6.0" - regenerator-runtime "^0.13.7" - stable "^0.1.8" - store2 "^2.7.1" ts-dedent "^2.0.0" util-deprecate "^1.0.2" @@ -5637,6 +4880,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/client-api@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.17.tgz#3ced22f08a47af70ccf8929111bc44b79e9e8ec0" + integrity sha512-Loz/wdh0axgq0PS19tx0tGEFEkFWlYc6YauJGHjygYa1xX7mJ54hDoaTolySCXN1HtfZn08D847yjGSN2oIqVg== + dependencies: + "@storybook/addons" "6.1.17" + "@storybook/channel-postmessage" "6.1.17" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/csf" "0.0.1" + "@types/qs" "^6.9.0" + "@types/webpack-env" "^1.15.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + regenerator-runtime "^0.13.7" + stable "^0.1.8" + store2 "^2.7.1" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-logger@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.11.tgz#5dd092e4293e5f58f7e89ddbc6eb2511b7d60954" @@ -5653,6 +4920,14 @@ core-js "^3.0.1" global "^4.3.2" +"@storybook/client-logger@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.17.tgz#0d89aaf824457f19bf9aa585bbcada57595e7d01" + integrity sha512-oqExrxhmws0ihB47sjdynZHd3OpUP4KWkx4udG+74lYIvBH+EZmQ9xF+UofeY3j5p1I9k8ugEcVKy0sqh1yR3w== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.11.tgz#edd5db7fe43f47b5a7ab515840795a89d931512e" @@ -5705,6 +4980,32 @@ react-textarea-autosize "^8.1.1" ts-dedent "^2.0.0" +"@storybook/components@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.17.tgz#f92d36e370ec6039d8c7cee9ef13dda866eed3da" + integrity sha512-rIEll0UTxEKmG4IsSS5K+6DjRLVtX8J+9cg79GSAC7N1ZHUR1UQmjjJaehJa5q/NQ5H8C39acxpT4Py/BcsL2g== + dependencies: + "@popperjs/core" "^2.5.4" + "@storybook/client-logger" "6.1.17" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.1.17" + "@types/overlayscrollbars" "^1.9.0" + "@types/react-color" "^3.0.1" + "@types/react-syntax-highlighter" "11.0.4" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.11.4" + memoizerific "^1.11.3" + overlayscrollbars "^1.10.2" + polished "^3.4.4" + react-color "^2.17.0" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.0" + react-textarea-autosize "^8.1.1" + ts-dedent "^2.0.0" + "@storybook/core-events@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.11.tgz#d50e8ec90490f9a7180a8c8a83afb6dcfe47ed66" @@ -5719,6 +5020,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.17.tgz#697ed916fcb2a411bc9f8bdbfacd0eb9d394eb58" + integrity sha512-xBI7kmyROcqhYNmFv4QBjD77CzV+k/0F051YFS5WicEI4qDWPPvzaShhm96ZrGobUX3+di4pC11gqdsrFeNCEg== + dependencies: + core-js "^3.0.1" + "@storybook/core@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa" @@ -5895,6 +5203,18 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.17.tgz#96746878c50c6c97c7de5a1b23a9503c5d648775" + integrity sha512-wLqSOB5yLXgNyDGy008RUvjVRtVMq7lhmMRicSIxgJpkakPrMRN8n/nK7pxgQc/xDTphnS0u1nT01i97WszhCg== + dependencies: + "@reach/router" "^1.3.3" + "@types/reach__router" "^1.3.7" + 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" @@ -5956,6 +5276,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.17.tgz#99cc120a230c30458d833b40c806b9b4dff7b34a" + integrity sha512-LpRuY2aIh2td+qZi7E8cp2oM88LudNMmTsBT6N2/Id69u/a9qQd2cYCA9k9fAsg7rjor+wR/N695jk3SGtoFTw== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.23" + "@storybook/client-logger" "6.1.17" + 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 "^2.0.0" + "@storybook/ui@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6" @@ -6133,30 +5471,11 @@ resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== -"@svgr/babel-plugin-transform-svg-component@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.4.0.tgz#a2212b4d018e6075a058bb7e220a66959ef7a03c" - integrity sha512-zLl4Fl3NvKxxjWNkqEcpdSOpQ3LGVH2BNFQ6vjaK6sFo2IrSznrhURIPI0HAphKiiIwNYjAfE0TNoQDSZv0U9A== - "@svgr/babel-plugin-transform-svg-component@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== -"@svgr/babel-preset@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.4.0.tgz#da21854643e1c4ad2279239baa7d5a8b128c1f15" - integrity sha512-Gyx7cCxua04DBtyILTYdQxeO/pwfTBev6+eXTbVbxe4HTGhOUW6yo7PSbG2p6eJMl44j6XSequ0ZDP7bl0nu9A== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" - "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" - "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" - "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" - "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" - "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" - "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" - "@svgr/babel-plugin-transform-svg-component" "^5.4.0" - "@svgr/babel-preset@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" @@ -6180,13 +5499,6 @@ camelcase "^6.2.0" cosmiconfig "^7.0.0" -"@svgr/hast-util-to-babel-ast@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.4.0.tgz#bb5d002e428f510aa5b53ec0a02377a95b367715" - integrity sha512-+U0TZZpPsP2V1WvVhqAOSTk+N+CjYHdZx+x9UBa1eeeZDXwH8pt0CrQf2+SvRl/h2CAPRFkm+Ey96+jKP8Bsgg== - dependencies: - "@babel/types" "^7.9.5" - "@svgr/hast-util-to-babel-ast@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" @@ -6194,17 +5506,7 @@ dependencies: "@babel/types" "^7.12.6" -"@svgr/plugin-jsx@5.4.x": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.4.0.tgz#ab47504c55615833c6db70fca2d7e489f509787c" - integrity sha512-SGzO4JZQ2HvGRKDzRga9YFSqOqaNrgLlQVaGvpZ2Iht2gwRp/tq+18Pvv9kS9ZqOMYgyix2LLxZMY1LOe9NPqw== - dependencies: - "@babel/core" "^7.7.5" - "@svgr/babel-preset" "^5.4.0" - "@svgr/hast-util-to-babel-ast" "^5.4.0" - svg-parser "^2.0.2" - -"@svgr/plugin-jsx@^5.4.0", "@svgr/plugin-jsx@^5.5.0": +"@svgr/plugin-jsx@5.5.x", "@svgr/plugin-jsx@^5.4.0", "@svgr/plugin-jsx@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== @@ -6600,11 +5902,9 @@ integrity sha512-Xwy8o12ak+iYgFr/KCVaVK5Sy+jFMiiPAID3+ObvMlBzy26XQJw5xu+a6rlHsrJENXj/AwJOGsJpVohUjAzSKQ== "@types/cors@^2.8.4", "@types/cors@^2.8.6": - version "2.8.6" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" - integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== - dependencies: - "@types/express" "*" + version "2.8.9" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz#4bd1fcac72eca8d5bec93e76c7fdcbdc1bc2cd4a" + integrity sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg== "@types/cssnano@*": version "4.0.0" @@ -6868,9 +6168,9 @@ "@types/node" "*" "@types/http-proxy@*", "@types/http-proxy@^1.17.4": - version "1.17.4" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" - integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== + version "1.17.5" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d" + integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q== dependencies: "@types/node" "*" @@ -8873,6 +8173,21 @@ autoprefixer@^9.7.2: postcss "^7.0.32" postcss-value-parser "^4.1.0" +aws-sdk@^2.840.0: + version "2.840.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.840.0.tgz#f5529c9bd3bf0be7f8e855a23ff9c12b1705418f" + integrity sha512-ngesHJqb0PXYjJNnCsAX4yLkR6JFQJB+3eDGwh3mYRjcq9voix5RfbCFQT1lwWu7bcMBPCrRIA2lJkkTMYXq+A== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -9522,11 +8837,6 @@ bowser@^1.7.3: resolved "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== -bowser@^2.11.0: - version "2.11.0" - resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" - integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== - boxen@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -9756,7 +9066,7 @@ buffer-xor@^1.0.3: resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@^4.3.0: +buffer@4.9.2, buffer@^4.3.0: version "4.9.2" resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== @@ -12030,6 +11340,11 @@ depd@~2.0.0: resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +dependency-graph@^0.10.0: + version "0.10.0" + resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.10.0.tgz#dfebe384f1f36faf7782be203a7a71102a6335a6" + integrity sha512-c9amUgpgxSi1bE5/sbLwcs5diLD0ygCQYmhfM5H1s5VH1mCsYkcmAL3CcNdv4kdSw6JuMoHeDGzLgj/gAXdWVg== + dependency-graph@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318" @@ -13199,6 +12514,11 @@ eventemitter3@^4.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== +events@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + events@3.1.0, events@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" @@ -13496,11 +12816,6 @@ extsprintf@^1.2.0: resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-base64-decode@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" - integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== - fast-deep-equal@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -13570,11 +12885,6 @@ fast-text-encoding@^1.0.0: resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== -fast-xml-parser@^3.16.0: - version "3.17.6" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.6.tgz#4f5df8cf927c3e59a10362abcfb7335c34bc5c5f" - integrity sha512-40WHI/5d2MOzf1sD2bSaTXlPn1lueJLAX6j1xH5dSAr6tNeut8B9ktEL6sjAK9yVON4uNj9//axOdBJUuruCzw== - fastest-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" @@ -15476,6 +14786,11 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -16947,6 +16262,11 @@ jest@^26.0.1: import-local "^3.0.2" jest-cli "^26.6.3" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + jose@^1.27.1: version "1.28.0" resolved "https://registry.npmjs.org/jose/-/jose-1.28.0.tgz#0803f8c71f43cd293a9d931c555c30531f5ca5dc" @@ -18190,7 +17510,7 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" -luxon@^1.25.0: +luxon@1.25.0, luxon@^1.25.0: version "1.25.0" resolved "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== @@ -21327,9 +20647,9 @@ prettier@^2.0.5, prettier@~2.0.5: integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== 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== + version "5.5.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== pretty-error@^2.1.1: version "2.1.1" @@ -22069,13 +21389,6 @@ react-motion@^0.5.2: prop-types "^15.5.8" raf "^3.1.0" -react-native-get-random-values@^1.4.0: - version "1.5.0" - resolved "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.5.0.tgz#91cda18f0e66e3d9d7660ba80c61c914030c1e05" - integrity sha512-LK+Wb8dEimJkd/dub7qziDmr9Tw4chhpzVeQ6JDo4czgfG4VXbptRyOMdu8503RiMF6y9pTH6ZUTkrrpprqT7w== - dependencies: - fast-base64-decode "^1.0.0" - react-popper-tooltip@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-3.1.1.tgz#329569eb7b287008f04fcbddb6370452ad3f9eac" @@ -23313,6 +22626,11 @@ sanitize-html@^1.27.0: srcset "^2.0.1" xtend "^4.0.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -25391,7 +24709,12 @@ tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1 resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@~2.0.0, tslib@~2.0.1: +tslib@^2.0.0, tslib@^2.0.1, tslib@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + +tslib@~2.0.0, tslib@~2.0.1: version "2.0.3" resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== @@ -25863,6 +25186,14 @@ url-parse@^1.4.3, url-parse@^1.4.7: querystringify "^2.1.1" requires-port "^1.0.0" +url@0.10.3: + version "0.10.3" + resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -25961,7 +25292,12 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -26682,6 +26018,14 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml2js@^0.4.19, xml2js@^0.4.23: version "0.4.23" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -26695,6 +26039,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@~9.0.1: + version "9.0.7" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
export type BackstageIdentity = { /** - * The Backstage user ID. + * The backstage user ID. */ id: string; @@ -91,6 +95,6 @@ export type BackstageIdentity = {
{YAML.stringify(configFile.config)}
catalog-info.yaml
+ https://github.com/backstage/backstage/blob/master/catalog-info.yaml +
https://github.com/backstage/backstage