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/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/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/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/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/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-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/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/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 2991a6babd..48f2f81d9f 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -249,6 +249,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/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/README.md b/docs/features/search/README.md index cd971d9d41..a06b9aee2b 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -24,12 +24,18 @@ Backstage ecosystem. ## Project roadmap -| Version | Description | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Backstage Search V.0 ✅ | Search Frontend letting you search through the entities of the software catalog. [See V.0 Use Cases.](#backstage-search-v0) | -| Backstage Search V.1 ⌛ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See V.1 Use Cases.](#backstage-search-v1) | -| Backstage Search V.2 ⌛ | Search Backend responsible for the indexing process of entities, and their metadata, registered to the Software Catalog. [See V.2 Use Cases.](#backstage-search-v2) | -| Backstage Search V.3 ⌛ | Standardized Search API lets you index other plugins data to the search engine of choice. [See V.3 Use Cases.](#backstage-search-v3) | +| Version | Description | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Backstage Search v0 ✅ | Search Frontend letting you search through the entities of the software catalog. [See v0 Use Cases.](#backstage-search-v0) | +| [Backstage Search V0.5 ⌛][v0.5] | Foundations for the architecture. | +| [Backstage Search v1 ⌛][v1] | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See v1 Use Cases.](#backstage-search-v1) | +| [Backstage Search v2 ⌛][v2] | Search Backend responsible for the indexing process of entities, and their metadata, registered to the Software Catalog. [See v2 Use Cases.](#backstage-search-v2) | +| [Backstage Search v3 ⌛][v3] | Standardized Search API lets you index other plugins data to the search engine of choice. [See v3 Use Cases.](#backstage-search-v3) | + +[v0.5]: https://github.com/backstage/backstage/milestone/25 +[v1]: https://github.com/backstage/backstage/milestone/26 +[v2]: https://github.com/backstage/backstage/milestone/27 +[v3]: https://github.com/backstage/backstage/milestone/28 ## Use Cases 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 9b82e0bb32..88d039e7c8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,40 +1,39 @@ { "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/plugin-welcome": "^0.2.5", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -54,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/app/src/plugins.ts b/packages/app/src/plugins.ts index 70dd56df0d..c62c0435a2 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin as WelcomePlugin } from '@backstage/plugin-welcome'; export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; 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 faec36dab4..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", @@ -50,6 +50,7 @@ "knex": "^0.21.6", "lodash": "^4.17.15", "logform": "^2.1.1", + "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", "selfsigned": "^1.10.7", @@ -68,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/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index a08849813d..2f3b904d38 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -63,10 +63,10 @@ export function errorHandler( return ( error: Error, _request: Request, - response: Response, + res: Response, next: NextFunction, ) => { - if (response.headersSent) { + if (res.headersSent) { // If the headers have already been sent, do not send the response again // as this will throw an error in the backend. next(error); @@ -80,7 +80,9 @@ export function errorHandler( logger.error(error); } - response.status(status).send(message); + res.status(status); + res.setHeader('content-type', 'text/plain'); + res.send(message); }; } diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 578db2ac92..bc1b61a041 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -29,6 +29,7 @@ import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchResponse, UrlReader, } from './types'; import { ReadTreeResponseFactory } from './tree'; @@ -116,6 +117,10 @@ export class AzureUrlReader implements UrlReader { }); } + async search(): Promise { + throw new Error('AzureUrlReader does not implement search'); + } + toString() { const { host, token } = this.options; return `azure{host=${host},authed=${Boolean(token)}}`; diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 373c75588e..e51c03e562 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -31,6 +31,7 @@ import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchResponse, UrlReader, } from './types'; @@ -129,6 +130,10 @@ export class BitbucketUrlReader implements UrlReader { }); } + async search(): Promise { + throw new Error('BitbucketUrlReader does not implement search'); + } + toString() { const { host, token, username, appPassword } = this.config; let authed = Boolean(token); diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts index 81f1dfa90e..0b26dfe911 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -16,7 +16,12 @@ import fetch from 'cross-fetch'; import { NotFoundError } from '../errors'; -import { ReaderFactory, ReadTreeResponse, UrlReader } from './types'; +import { + ReaderFactory, + ReadTreeResponse, + SearchResponse, + UrlReader, +} from './types'; /** * A UrlReader that does a plain fetch of the URL. @@ -68,10 +73,14 @@ export class FetchUrlReader implements UrlReader { throw new Error(message); } - readTree(): Promise { + async readTree(): Promise { throw new Error('FetchUrlReader does not implement readTree'); } + async search(): Promise { + throw new Error('FetchUrlReader does not implement search'); + } + toString() { return 'fetch{}'; } diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index fae3f3ba12..d4e579b143 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -23,7 +23,13 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; import { NotFoundError, NotModifiedError } from '../errors'; -import { GithubUrlReader } from './GithubUrlReader'; +import { + GhBlobResponse, + GhBranchResponse, + GhRepoResponse, + GhTreeResponse, + GithubUrlReader, +} from './GithubUrlReader'; import { ReadTreeResponseFactory } from './tree'; const treeResponseFactory = ReadTreeResponseFactory.create({ @@ -69,6 +75,10 @@ describe('GithubUrlReader', () => { }); }); + /* + * read + */ + describe('read', () => { it('should use the headers from the credentials provider to the fetch request when doing read', async () => { expect.assertions(2); @@ -107,6 +117,10 @@ describe('GithubUrlReader', () => { }); }); + /* + * readTree + */ + describe('readTree', () => { beforeEach(() => { mockFs({ @@ -128,29 +142,31 @@ describe('GithubUrlReader', () => { ); const reposGithubApiResponse = { - id: '123', + id: 123, full_name: 'backstage/mock', default_branch: 'main', branches_url: 'https://api.github.com/repos/backstage/mock/branches{/branch}', archive_url: 'https://api.github.com/repos/backstage/mock/{archive_format}{/ref}', - }; + } as Partial; const reposGheApiResponse = { - ...reposGithubApiResponse, + id: 123, + full_name: 'backstage/mock', + default_branch: 'main', branches_url: 'https://ghe.github.com/api/v3/repos/backstage/mock/branches{/branch}', archive_url: 'https://ghe.github.com/api/v3/repos/backstage/mock/{archive_format}{/ref}', - }; + } as Partial; const branchesApiResponse = { name: 'main', commit: { sha: 'etag123abc', }, - }; + } as Partial; beforeEach(() => { worker.use( @@ -392,4 +408,359 @@ describe('GithubUrlReader', () => { }).toThrowError('must configure an explicit apiBaseUrl'); }); }); + + /* + * search + */ + + describe('search', () => { + beforeEach(() => { + mockFs({ '/tmp': mockFs.directory() }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + const repoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'backstage-mock-etag123.tar.gz', + ), + ); + + const githubTreeContents: GhTreeResponse['tree'] = [ + { + path: 'mkdocs.yml', + type: 'blob', + url: 'https://api.github.com/repos/backstage/mock/git/blobs/1', + }, + { + path: 'docs', + type: 'tree', + url: 'https://api.github.com/repos/backstage/mock/git/trees/2', + }, + { + path: 'docs/index.md', + type: 'blob', + url: 'https://api.github.com/repos/backstage/mock/git/blobs/3', + }, + ]; + + const gheTreeContents: GhTreeResponse['tree'] = [ + { + path: 'mkdocs.yml', + type: 'blob', + url: 'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/1', + }, + { + path: 'docs', + type: 'tree', + url: 'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees/2', + }, + { + path: 'docs/index.md', + type: 'blob', + url: 'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/3', + }, + ]; + + // Tarballs + beforeEach(() => { + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/tarball/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-etag123.tar.gz', + ), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/tarball/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-etag123.tar.gz', + ), + ctx.body(repoBuffer), + ), + ), + ); + }); + + // Repo details + beforeEach(() => { + const githubResponse = { + id: 123, + full_name: 'backstage/mock', + default_branch: 'main', + branches_url: + 'https://api.github.com/repos/backstage/mock/branches{/branch}', + archive_url: + 'https://api.github.com/repos/backstage/mock/{archive_format}{/ref}', + trees_url: + 'https://api.github.com/repos/backstage/mock/git/trees{/sha}', + } as Partial; + + const gheResponse = { + id: 123, + full_name: 'backstage/mock', + default_branch: 'main', + branches_url: + 'https://ghe.github.com/api/v3/repos/backstage/mock/branches{/branch}', + archive_url: + 'https://ghe.github.com/api/v3/repos/backstage/mock/{archive_format}{/ref}', + trees_url: + 'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees{/sha}', + } as Partial; + + worker.use( + rest.get('https://api.github.com/repos/backstage/mock', (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(githubResponse), + ), + ), + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(gheResponse), + ), + ), + ); + }); + + // Branch details + beforeEach(() => { + const response = { + name: 'main', + commit: { + sha: 'etag123abc', + }, + } as Partial; + + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(response), + ), + ), + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(response), + ), + ), + rest.get( + 'https://api.github.com/repos/backstage/mock/branches/branchDoesNotExist', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + }); + + // Blobs + beforeEach(() => { + const blob1Response = { + content: Buffer.from('site_name: Test\n').toString('base64'), + } as Partial; + + const blob3Response = { + content: Buffer.from('# Test\n').toString('base64'), + } as Partial; + + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/git/blobs/1', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(blob1Response), + ), + ), + rest.get( + 'https://api.github.com/repos/backstage/mock/git/blobs/3', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(blob3Response), + ), + ), + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/1', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(blob1Response), + ), + ), + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/3', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(blob3Response), + ), + ), + ); + }); + + async function runTests(reader: GithubUrlReader, baseUrl: string) { + const r1 = await reader.search( + `${baseUrl}/backstage/mock/tree/main/**/*`, + ); + expect(r1.etag).toBe('etag123abc'); + expect(r1.files.length).toBe(2); + + const r2 = await reader.search( + `${baseUrl}/backstage/mock/tree/main/**/*`, + { etag: 'somethingElse' }, + ); + expect(r2.etag).toBe('etag123abc'); + expect(r2.files.length).toBe(2); + + const r3 = await reader.search(`${baseUrl}/backstage/mock/tree/main/o`); + expect(r3.files.length).toBe(0); + + const r4 = await reader.search( + `${baseUrl}/backstage/mock/tree/main/*docs*`, + ); + expect(r4.files.length).toBe(1); + expect(r4.files[0].url).toBe( + `${baseUrl}/backstage/mock/tree/main/mkdocs.yml`, + ); + await expect(r4.files[0].content()).resolves.toEqual( + Buffer.from('site_name: Test\n'), + ); + + const r5 = await reader.search( + `${baseUrl}/backstage/mock/tree/main/*/index.*`, + ); + expect(r5.files.length).toBe(1); + expect(r5.files[0].url).toBe( + `${baseUrl}/backstage/mock/tree/main/docs/index.md`, + ); + await expect(r5.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + } + + // eslint-disable-next-line jest/expect-expect + it('succeeds on github when going via repo listing', async () => { + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/git/trees/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + truncated: false, + tree: githubTreeContents, + } as Partial), + ), + ), + ); + await runTests(githubProcessor, 'https://github.com'); + }); + + // eslint-disable-next-line jest/expect-expect + it('succeeds on github when going via readTree', async () => { + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/git/trees/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + truncated: true, + tree: [], + } as Partial), + ), + ), + ); + await runTests(githubProcessor, 'https://github.com'); + }); + + // eslint-disable-next-line jest/expect-expect + it('succeeds on ghe when going via repo listing', async () => { + worker.use( + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + truncated: false, + tree: gheTreeContents, + } as Partial), + ), + ), + ); + await runTests(gheProcessor, 'https://ghe.github.com'); + }); + + // eslint-disable-next-line jest/expect-expect + it('succeeds on ghe when going via readTree', async () => { + worker.use( + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + truncated: true, + tree: [], + } as Partial), + ), + ), + ); + await runTests(gheProcessor, 'https://ghe.github.com'); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + githubProcessor.search( + 'https://githib.com/backstage/mock/tree/main/**/*', + { etag: 'etag123abc' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + + it('throws NotFoundError when missing branch', async () => { + await expect( + githubProcessor.search( + 'https://githib.com/backstage/mock/tree/branchDoesNotExist/**/*', + ), + ).rejects.toThrow(NotFoundError); + }); + }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 99d641f2dd..291740a9fc 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -23,6 +23,7 @@ import { import { RestEndpointMethodTypes } from '@octokit/rest'; import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; @@ -30,9 +31,17 @@ import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, + SearchResponse, + SearchResponseFile, UrlReader, } from './types'; +export type GhRepoResponse = RestEndpointMethodTypes['repos']['get']['response']['data']; +export type GhBranchResponse = RestEndpointMethodTypes['repos']['getBranch']['response']['data']; +export type GhTreeResponse = RestEndpointMethodTypes['git']['getTree']['response']['data']; +export type GhBlobResponse = RestEndpointMethodTypes['git']['getBlob']['response']['data']; + /** * A processor that adds the ability to read files from GitHub v3 APIs, such as * the one exposed by GitHub itself. @@ -106,27 +115,43 @@ export class GithubUrlReader implements UrlReader { throw new NotModifiedError(); } + const { filepath } = parseGitUrl(url); const { headers } = await this.deps.credentialsProvider.getCredentials({ url, }); - // archive_url looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}" - const archive = await this.fetchResponse( - repoDetails.repo.archive_url - .replace('{archive_format}', 'tarball') - .replace('{/ref}', `/${commitSha}`), + return this.doReadTree( + repoDetails.repo.archive_url, + commitSha, + filepath, + { headers }, + options, + ); + } + + async search(url: string, options?: SearchOptions): Promise { + const repoDetails = await this.getRepoDetails(url); + const commitSha = repoDetails.branch.commit.sha!; + + if (options?.etag && options.etag === commitSha) { + throw new NotModifiedError(); + } + + const { filepath } = parseGitUrl(url); + const { headers } = await this.deps.credentialsProvider.getCredentials({ + url, + }); + + const files = await this.doSearch( + url, + repoDetails.repo.trees_url, + repoDetails.repo.archive_url, + commitSha, + filepath, { headers }, ); - const { filepath } = parseGitUrl(url); - return await this.deps.treeResponseFactory.fromTarArchive({ - // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want - // to stick to using that in exclusively backend code. - stream: (archive.body as unknown) as Readable, - subpath: filepath, - etag: commitSha, - filter: options?.filter, - }); + return { files, etag: commitSha }; } toString() { @@ -134,11 +159,92 @@ export class GithubUrlReader implements UrlReader { return `github{host=${host},authed=${Boolean(token)}}`; } + private async doReadTree( + archiveUrl: string, + sha: string, + subpath: string, + init: RequestInit, + options?: ReadTreeOptions, + ): Promise { + // archive_url looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}" + const archive = await this.fetchResponse( + archiveUrl + .replace('{archive_format}', 'tarball') + .replace('{/ref}', `/${sha}`), + init, + ); + + return await this.deps.treeResponseFactory.fromTarArchive({ + // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want + // to stick to using that in exclusively backend code. + stream: (archive.body as unknown) as Readable, + subpath, + etag: sha, + filter: options?.filter, + }); + } + + private async doSearch( + url: string, + treesUrl: string, + archiveUrl: string, + sha: string, + query: string, + init: RequestInit, + ): Promise { + function pathToUrl(path: string): string { + // TODO(freben): Use the integration package facility for this instead + // pathname starts as /backstage/backstage/blob/master/ + const updated = new URL(url); + const base = updated.pathname.split('/').slice(1, 5).join('/'); + updated.pathname = `${base}/${path}`; + return updated.toString(); + } + + const matcher = new Minimatch(query.replace(/^\/+/, '')); + + // trees_url looks like "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}" + const recursiveTree: GhTreeResponse = await this.fetchJson( + treesUrl.replace('{/sha}', `/${sha}?recursive=true`), + init, + ); + + // The simple case is that we got the entire tree in a single operation. + if (!recursiveTree.truncated) { + const matching = recursiveTree.tree.filter( + item => + item.type === 'blob' && + item.path && + item.url && + matcher.match(item.path), + ); + + return matching.map(item => ({ + url: pathToUrl(item.path!), + content: async () => { + const blob: GhBlobResponse = await this.fetchJson(item.url!, init); + return Buffer.from(blob.content, 'base64'); + }, + })); + } + + // For larger repos, we leverage readTree and filter through that instead + const tree = await this.doReadTree(archiveUrl, sha, '', init, { + filter: path => matcher.match(path), + }); + const files = await tree.files(); + + return files.map(file => ({ + url: pathToUrl(file.path), + content: file.content, + })); + } + private async getRepoDetails( url: string, ): Promise<{ - repo: RestEndpointMethodTypes['repos']['get']['response']['data']; - branch: RestEndpointMethodTypes['repos']['getBranch']['response']['data']; + repo: GhRepoResponse; + branch: GhBranchResponse; }> { const parsed = parseGitUrl(url); const { ref, full_name } = parsed; @@ -151,13 +257,13 @@ export class GithubUrlReader implements UrlReader { url, }); - const repo: RestEndpointMethodTypes['repos']['get']['response']['data'] = await this.fetchJson( + const repo: GhRepoResponse = await this.fetchJson( `${this.config.apiBaseUrl}/repos/${full_name}`, { headers }, ); // branches_url looks like "https://api.github.com/repos/owner/repo/branches{/branch}" - const branch: RestEndpointMethodTypes['repos']['getBranch']['response']['data'] = await this.fetchJson( + const branch: GhBranchResponse = await this.fetchJson( repo.branches_url.replace('{/branch}', `/${ref || repo.default_branch}`), { headers }, ); 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 8a763d1259..66030e30d9 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -21,16 +21,17 @@ import { readGitLabIntegrationConfigs, } from '@backstage/integration'; import fetch from 'cross-fetch'; +import parseGitUrl from 'git-url-parse'; +import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchResponse, UrlReader, } from './types'; -import parseGitUrl from 'git-url-parse'; -import { Readable } from 'stream'; export class GitlabUrlReader implements UrlReader { private readonly treeResponseFactory: ReadTreeResponseFactory; @@ -51,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 { @@ -154,6 +149,10 @@ export class GitlabUrlReader implements UrlReader { }); } + async search(): Promise { + throw new Error('GitlabUrlReader does not implement search'); + } + toString() { const { host, token } = this.config; return `gitlab{host=${host},authed=${Boolean(token)}}`; diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts index 3183aa0c28..91f4ae63ab 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts @@ -18,6 +18,8 @@ import { NotAllowedError } from '../errors'; import { ReadTreeOptions, ReadTreeResponse, + SearchOptions, + SearchResponse, UrlReader, UrlReaderPredicateTuple, } from './types'; @@ -60,6 +62,18 @@ export class UrlReaderPredicateMux implements UrlReader { throw new NotAllowedError(`Reading from '${url}' is not allowed`); } + async search(url: string, options?: SearchOptions): Promise { + const parsed = new URL(url); + + for (const { predicate, reader } of this.readers) { + if (predicate(parsed)) { + return await reader.search(url, options); + } + } + + throw new NotAllowedError(`Reading from '${url}' is not allowed`); + } + toString() { return `predicateMux{readers=${this.readers.map(t => t.reader).join(',')}`; } diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 8a6c2bf1a3..1266161ddf 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export type { UrlReader, ReadTreeResponse } from './types'; +export type { UrlReader, ReadTreeResponse, SearchResponse } from './types'; export { UrlReaders } from './UrlReaders'; export { AzureUrlReader } from './AzureUrlReader'; export { BitbucketUrlReader } from './BitbucketUrlReader'; diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index be788fc6e2..4caa76841a 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -24,6 +24,7 @@ import { ReadTreeResponseFactory } from './tree'; export type UrlReader = { read(url: string): Promise; readTree(url: string, options?: ReadTreeOptions): Promise; + search(url: string, options?: SearchOptions): Promise; }; export type UrlReaderPredicateTuple = { @@ -103,3 +104,51 @@ export type ReadTreeResponseFile = { path: string; content(): Promise; }; + +/** + * An options object for search operations. + */ +export type SearchOptions = { + /** + * An etag can be provided to check whether the search response has changed from a previous execution. + * + * In the search() response, an etag is returned along with the files. The etag is a unique identifer + * of the current tree, usually the commit SHA or etag from the target. + * + * When an etag is given in SearchOptions, search will first compare the etag against the etag + * on the target branch. If they match, search will throw a NotModifiedError indicating that the search + * response will not differ from the previous response which included this particular etag. If they mismatch, + * search will return the rest of SearchResponse along with a new etag. + */ + etag?: string; +}; + +/** + * The output of a search operation. + */ +export type SearchResponse = { + /** + * The files that matched the search query. + */ + files: SearchResponseFile[]; + + /** + * A unique identifer of the current remote tree, usually the commit SHA or etag from the target. + */ + etag: string; +}; + +/** + * Represents a single file in a search response. + */ +export type SearchResponseFile = { + /** + * The full URL to the file. + */ + url: string; + + /** + * The binary contents of the file. + */ + content(): Promise; +}; 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 fb33a154a7..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", @@ -93,7 +93,7 @@ "rollup-plugin-esbuild": "2.6.x", "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^3.1.1", - "rollup-plugin-typescript2": "^0.27.3", + "rollup-plugin-typescript2": "^0.29.0", "rollup-pluginutils": "^2.8.2", "semver": "^7.3.2", "start-server-webpack-plugin": "^2.2.5", 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/Button/Button.stories.tsx b/packages/core/src/components/Button/Button.stories.tsx index a5005574fe..af95e64d52 100644 --- a/packages/core/src/components/Button/Button.stories.tsx +++ b/packages/core/src/components/Button/Button.stories.tsx @@ -15,8 +15,17 @@ */ import React, { FunctionComponentFactory } from 'react'; import { Button } from './Button'; -import { MemoryRouter, Route, useLocation } from 'react-router-dom'; +import { MemoryRouter, useLocation } from 'react-router-dom'; import { createRouteRef } from '@backstage/core-api'; +import { + Divider, + Link, + List, + ListItem, + ListItemText, + Typography, + Button as MaterialButton, +} from '@material-ui/core'; const Location = () => { const location = useLocation(); @@ -28,14 +37,27 @@ export default { component: Button, decorators: [ (storyFn: FunctionComponentFactory<{}>) => ( - -
+ <> + + A collection of buttons that should be used in the Backstage + interface. These leverage the properties inherited from{' '} + + Material-UI Button + + , but include an opinionated set that align to the Backstage design. + + + + +
- +
+ +
+ {storyFn()}
- {storyFn()} -
-
+ + ), ], }; @@ -46,36 +68,107 @@ export const Default = () => { title: 'Hi there!', }); + // Design Permutations: + // color = default | primary | secondary + // variant = contained | outlined | text return ( - <> -  will utilise the - react-router MemoryRouter's navigation - -

{routeRef.title}

-
- + + + + Default Button: + This is the default button design which should be used in most cases. +
+
color="primary" variant="contained"
+
+ + +
+ + + Secondary Button: + Used for actions that cancel, skip, and in general perform negative + functions, etc. +
+
color="secondary" variant="contained"
+
+ + +
+ + + Tertiary Button: + Used commonly in a ButtonGroup and when the button function itself is + not a primary function on a page. +
+
color="default" variant="outlined"
+
+ + +
+
); }; -export const PassProps = () => { +export const ButtonLinks = () => { const routeRef = createRouteRef({ path: '/hello', title: 'Hi there!', }); + const handleClick = () => { + return 'Your click worked!'; + }; + return ( <> - -  has props for both material-ui's component as well as for - react-router-dom's - -

{routeRef.title}

-
+ + { + // TODO: Refactor to use new routing mechanisms + } + + +   has props for both Material-UI's component as well as for + react-router-dom's Route object. + + + + +   links to a statically defined route. In general, this should be + avoided. + + + + + View URL + +   links to a defined URL using Material-UI's Button. + + + + + Trigger Event + +   triggers an onClick event using Material-UI's Button. + + ); }; -PassProps.story = { - name: `Accepts material-ui Button's and react-router-dom Link's props`, -}; 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 740a08aaa2..6520f0f8e4 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -14,14 +14,18 @@ * limitations under the License. */ +import { + ReadTreeResponse, + SearchResponse, + UrlReader, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import { Readable } from 'stream'; import { getDocFilesFromRepository, getLocationForEntity, parseReferenceAnnotation, } from './helpers'; -import { UrlReader, ReadTreeResponse } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; const entityBase: Entity = { metadata: { @@ -135,7 +139,14 @@ describe('getDocFilesFromRepository', () => { archive: async () => { return Readable.from(''); }, + etag: 'etag123abc', + }; + } + + async search(): Promise { + return { etag: '', + files: [], }; } } @@ -145,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 630608e817..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(); @@ -49,6 +50,7 @@ const mockConfig = new ConfigReader({}); const mockUrlReader: jest.Mocked = { read: jest.fn(), readTree: jest.fn(), + search: jest.fn(), }; describe('directory preparer', () => { @@ -65,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 () => { @@ -83,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 () => { @@ -101,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/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx index 494695e277..bcb310be46 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx @@ -61,7 +61,7 @@ describe('ApiCatalogTable component', () => { ), ); const errorMessage = await rendered.findByText( - /Error encountered while fetching catalog entities./, + /Could not fetch catalog entities./, ); expect(errorMessage).toBeInTheDocument(); }); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 101c28332a..701ca457df 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -22,11 +22,13 @@ import { RELATION_PART_OF, } from '@backstage/catalog-model'; import { + CodeSnippet, Table, TableColumn, TableFilter, TableState, useQueryParamState, + WarningPanel, } from '@backstage/core'; import { EntityRefLink, @@ -35,7 +37,6 @@ import { getEntityRelations, } from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; import React from 'react'; import { ApiTypeTitle } from '../ApiDefinitionCard'; @@ -151,11 +152,9 @@ export const ApiExplorerTable = ({ if (error) { return ( -
- - Error encountered while fetching catalog entities. {error.toString()} - -
+ + + ); } 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/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index 65cbddaae6..4379fcccb9 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -26,8 +26,34 @@ import fs from 'fs-extra'; export interface RouterOptions { config: Config; logger: Logger; + + /** + * The name of the app package that content should be served from. The same app package should be + * added as a dependency to the backend package in order for it to be accessible at runtime. + * + * In a typical setup with a single app package this would be set to 'app'. + */ appPackageName: string; + + /** + * A request handler to handle requests for static content that are not present in the app bundle. + * + * This can be used to avoid issues with clients on older deployment versions trying to access lazy + * loaded content that is no longer present. Typically the requests would fall back to a long-term + * object store where all recently deployed versions of the app are present. + */ staticFallbackHandler?: express.Handler; + + /** + * Disables the configuration injection. This can be useful if you're running in an environment + * with a read-only filesystem, or for some other reason don't want configuration to be injected. + * + * Note that this will cause the configuration used when building the app bundle to be used, unless + * a separate configuration loading strategy is set up. + * + * This also disables configuration injection though `APP_CONFIG_` environment variables. + */ + disableConfigInjection?: boolean; } export async function createRouter( @@ -48,13 +74,15 @@ export async function createRouter( logger.info(`Serving static app content from ${appDistDir}`); - const appConfigs = await readConfigs({ - config, - appDistDir, - env: process.env, - }); + if (!options.disableConfigInjection) { + const appConfigs = await readConfigs({ + config, + appDistDir, + env: process.env, + }); - await injectConfig({ appConfigs, logger, staticDir }); + await injectConfig({ appConfigs, logger, staticDir }); + } const router = Router(); 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/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index ba97232810..467c0eb2e8 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -130,7 +130,7 @@ export class HigherOrderOperations implements HigherOrderOperation { `Locations Refresh: Refreshing location ${location.type}:${location.target}`, ); try { - await this.refreshSingleLocation(location); + await this.refreshSingleLocation(location, logger); await this.locationsCatalog.logUpdateSuccess(location.id, undefined); } catch (e) { logger.warn( @@ -148,8 +148,12 @@ export class HigherOrderOperations implements HigherOrderOperation { } // Performs a full refresh of a single location - private async refreshSingleLocation(location: Location) { + private async refreshSingleLocation( + location: Location, + optionalLogger?: Logger, + ) { let startTimestamp = process.hrtime(); + const logger = optionalLogger || this.logger; const readerOutput = await this.locationReader.read({ type: location.type, @@ -157,12 +161,12 @@ export class HigherOrderOperations implements HigherOrderOperation { }); for (const item of readerOutput.errors) { - this.logger.warn( + logger.warn( `Failed item in location ${item.location.type}:${item.location.target}, ${item.error.stack}`, ); } - this.logger.info( + logger.info( `Read ${readerOutput.entities.length} entities from location ${ location.type }:${location.target} in ${durationText(startTimestamp)}`, @@ -186,14 +190,14 @@ export class HigherOrderOperations implements HigherOrderOperation { throw e; } - this.logger.debug(`Posting update success markers`); + logger.debug(`Posting update success markers`); await this.locationsCatalog.logUpdateSuccess( location.id, readerOutput.entities.map(e => e.entity.metadata.name), ); - this.logger.info( + logger.info( `Wrote ${readerOutput.entities.length} entities from location ${ location.type }:${location.target} in ${durationText(startTimestamp)}`, diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index a789e669d0..be88aa2810 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -139,6 +139,16 @@ export class LocationReaders implements LocationReader { if (emitResult.type === 'relation') { throw new Error('readLocation may not emit entity relations'); } + if ( + emitResult.type === 'location' && + emitResult.location.type === item.location.type && + emitResult.location.target === item.location.target + ) { + // Ignore self-referential locations silently (this can happen for + // example if you use a glob target like "**/*.yaml" in a Location + // entity) + return; + } emit(emitResult); }; 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/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts index be4f15e501..eb1187eda5 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts @@ -160,7 +160,7 @@ describe('CodeOwnersProcessor', () => { const read = jest .fn() .mockResolvedValue(mockReadResult({ data: ownersText })); - const reader = { read, readTree: jest.fn() }; + const reader = { read, readTree: jest.fn(), search: jest.fn() }; const result = await findRawCodeOwners(mockLocation(), { reader, logger, @@ -170,7 +170,7 @@ describe('CodeOwnersProcessor', () => { it('should return undefined when no codeowner', async () => { const read = jest.fn().mockRejectedValue(mockReadResult()); - const reader = { read, readTree: jest.fn() }; + const reader = { read, readTree: jest.fn(), search: jest.fn() }; await expect( findRawCodeOwners(mockLocation(), { reader, logger }), @@ -184,7 +184,7 @@ describe('CodeOwnersProcessor', () => { .mockImplementationOnce(() => mockReadResult({ error: 'foo' })) .mockImplementationOnce(() => mockReadResult({ error: 'bar' })) .mockResolvedValue(mockReadResult({ data: ownersText })); - const reader = { read, readTree: jest.fn() }; + const reader = { read, readTree: jest.fn(), search: jest.fn() }; const result = await findRawCodeOwners(mockLocation(), { reader, @@ -206,7 +206,7 @@ describe('CodeOwnersProcessor', () => { const read = jest .fn() .mockResolvedValue(mockReadResult({ data: mockCodeOwnersText() })); - const reader = { read, readTree: jest.fn() }; + const reader = { read, readTree: jest.fn(), search: jest.fn() }; const owner = await resolveCodeOwner(mockLocation(), { reader, logger }); expect(owner).toBe('backstage-core'); @@ -216,7 +216,7 @@ describe('CodeOwnersProcessor', () => { const read = jest .fn() .mockImplementation(() => mockReadResult({ error: 'error: foo' })); - const reader = { read, readTree: jest.fn() }; + const reader = { read, readTree: jest.fn(), search: jest.fn() }; await expect( resolveCodeOwner(mockLocation(), { reader, logger }), @@ -230,7 +230,7 @@ describe('CodeOwnersProcessor', () => { const read = jest .fn() .mockResolvedValue(mockReadResult({ data: mockCodeOwnersText() })); - const reader = { read, readTree: jest.fn() }; + const reader = { read, readTree: jest.fn(), search: jest.fn() }; const processor = new CodeOwnersProcessor({ reader, logger }); return { entity, processor, read }; 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/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts index 8b2632ad5d..00b6af94e5 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts @@ -27,7 +27,7 @@ import { describe('PlaceholderProcessor', () => { const read: jest.MockedFunction = jest.fn(); - const reader: UrlReader = { read, readTree: jest.fn() }; + const reader: UrlReader = { read, readTree: jest.fn(), search: jest.fn() }; beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index 49b6fe71e0..db731b5b17 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -14,24 +14,29 @@ * limitations under the License. */ -import { UrlReaderProcessor } from './UrlReaderProcessor'; -import { getVoidLogger, UrlReaders } from '@backstage/backend-common'; +import { + getVoidLogger, + UrlReader, + UrlReaders, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { msw } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, } from './types'; +import { UrlReaderProcessor } from './UrlReaderProcessor'; import { defaultEntityDataParser } from './util/parse'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; - const server = setupServer(); + const server = setupServer(); msw.setupDefaultHandlers(server); + it('should load from url', async () => { const logger = getVoidLogger(); const reader = UrlReaders.default({ @@ -57,7 +62,7 @@ describe('UrlReaderProcessor', () => { )) as CatalogProcessorEntityResult; expect(generated.type).toBe('entity'); - expect(generated.location).toBe(spec); + expect(generated.location).toEqual(spec); expect(generated.entity).toEqual({ mock: 'entity' }); }); @@ -92,4 +97,27 @@ describe('UrlReaderProcessor', () => { `Unable to read url, NotFoundError: could not read ${mockApiOrigin}/component-notfound.yaml, 404 Not Found`, ); }); + + it('uses search when there are globs', async () => { + const logger = getVoidLogger(); + + const reader: jest.Mocked = { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn().mockImplementation(async () => []), + }; + + const processor = new UrlReaderProcessor({ reader, logger }); + + const emit = jest.fn(); + + await processor.readLocation( + { type: 'url', target: 'https://github.com/a/b/blob/x/**/b.yaml' }, + false, + emit, + defaultEntityDataParser, + ); + + expect(reader.search).toBeCalledTimes(1); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts index 9daae29d3b..b70ef2877d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts @@ -16,6 +16,8 @@ import { UrlReader } from '@backstage/backend-common'; import { LocationSpec } from '@backstage/catalog-model'; +import parseGitUrl from 'git-url-parse'; +import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import * as result from './results'; import { @@ -59,10 +61,14 @@ export class UrlReaderProcessor implements CatalogProcessor { } try { - const data = await this.options.reader.read(location.target); - - for await (const parseResult of parser({ data, location })) { - emit(parseResult); + const output = await this.doRead(location.target); + for (const item of output) { + for await (const parseResult of parser({ + data: item.data, + location: { type: location.type, target: item.url }, + })) { + emit(parseResult); + } } } catch (error) { const message = `Unable to read ${location.type}, ${error}`; @@ -78,4 +84,25 @@ export class UrlReaderProcessor implements CatalogProcessor { return true; } + + private async doRead( + location: string, + ): Promise<{ data: Buffer; url: string }[]> { + // Does it contain globs? I.e. does it contain asterisks or question marks + // (no curly braces for now) + const { filepath } = parseGitUrl(location); + if (filepath?.match(/[*?]/)) { + const limiter = limiterFactory(5); + const response = await this.options.reader.search(location); + const output = response.files.map(async file => ({ + url: file.url, + data: await limiter(file.content), + })); + return Promise.all(output); + } + + // Otherwise do a plain read + const data = await this.options.reader.read(location); + return [{ url: location, data }]; + } } 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.test.ts b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts index 695902fec9..bc58338fce 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts @@ -44,6 +44,7 @@ describe('CatalogBuilder', () => { const reader: jest.Mocked = { read: jest.fn(), readTree: jest.fn(), + search: jest.fn(), }; const env: CatalogEnvironment = { logger: getVoidLogger(), 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-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 6064164e31..17cfc77d8b 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { errorHandler, NotFoundError } from '@backstage/backend-common'; import { locationSpecSchema, analyzeLocationSchema, @@ -57,7 +57,7 @@ export async function createRouter( const filter = EntityFilters.ofQuery(req.query); const fieldMapper = translateQueryToFieldMapper(req.query); const entities = await entitiesCatalog.entities(filter); - res.status(200).send(entities.map(fieldMapper)); + res.status(200).json(entities.map(fieldMapper)); }) .post('/entities', async (req, res) => { const body = await requireRequestBody(req); @@ -67,7 +67,7 @@ export async function createRouter( const [entity] = await entitiesCatalog.entities( EntityFilters.ofMatchers({ 'metadata.uid': result.entityId }), ); - res.status(200).send(entity); + res.status(200).json(entity); }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; @@ -75,15 +75,14 @@ export async function createRouter( EntityFilters.ofMatchers({ 'metadata.uid': uid }), ); if (!entities.length) { - res.status(404).send(`No entity with uid ${uid}`); - } else { - res.status(200).send(entities[0]); + throw new NotFoundError(`No entity with uid ${uid}`); } + res.status(200).json(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid); - res.status(204).send(); + res.status(204).end(); }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; @@ -95,14 +94,11 @@ export async function createRouter( }), ); if (!entities.length) { - res - .status(404) - .send( - `No entity with kind ${kind} namespace ${namespace} name ${name}`, - ); - } else { - res.status(200).send(entities[0]); + throw new NotFoundError( + `No entity with kind ${kind} namespace ${namespace} name ${name}`, + ); } + res.status(200).json(entities[0]); }); } @@ -111,7 +107,7 @@ export async function createRouter( const input = await validateRequestBody(req, locationSpecSchema); const dryRun = yn(req.query.dryRun, { default: false }); const output = await higherOrderOperation.addLocation(input, { dryRun }); - res.status(201).send(output); + res.status(201).json(output); }); } @@ -119,22 +115,22 @@ export async function createRouter( router .get('/locations', async (_req, res) => { const output = await locationsCatalog.locations(); - res.status(200).send(output); + res.status(200).json(output); }) .get('/locations/:id/history', async (req, res) => { const { id } = req.params; const output = await locationsCatalog.locationHistory(id); - res.status(200).send(output); + res.status(200).json(output); }) .get('/locations/:id', async (req, res) => { const { id } = req.params; const output = await locationsCatalog.location(id); - res.status(200).send(output); + res.status(200).json(output); }) .delete('/locations/:id', async (req, res) => { const { id } = req.params; await locationsCatalog.removeLocation(id); - res.status(204).send(); + res.status(204).end(); }); } @@ -142,7 +138,7 @@ export async function createRouter( router.post('/analyze-location', async (req, res) => { const input = await validateRequestBody(req, analyzeLocationSchema); const output = await locationAnalyzer.analyzeLocation(input); - res.status(200).send(output); + res.status(200).json(output); }); } 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. +![Catalog Import Plugin](./docs/catalog-import-screenshot.png) -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 ( +
+
+ ); +}; + +export const BackButton = (props: ComponentProps) => { + const classes = useStyles(); + + return ( + + ); +}; 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} - - - {errorOccurred ? ( - - ) : 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} - - )} - - - -
- ); -}; 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} - - - - ); -}; 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( + ( + <> + + {' '} + + )} + 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( + ( + <> + + + + )} + 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 + + )} + {' '} + + )} + 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/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 46d6010f2f..16c91c0d6c 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -50,7 +50,7 @@ describe('CatalogTable component', () => { ), ); const errorMessage = await rendered.findByText( - /Error encountered while fetching catalog entities./, + /Could not fetch catalog entities./, ); expect(errorMessage).toBeInTheDocument(); }); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 30a19145f2..cdc7451601 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -19,7 +19,13 @@ import { RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; -import { Table, TableColumn, TableProps } from '@backstage/core'; +import { + CodeSnippet, + Table, + TableColumn, + TableProps, + WarningPanel, +} from '@backstage/core'; import { EntityRefLink, EntityRefLinks, @@ -29,7 +35,6 @@ import { import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; -import { Alert } from '@material-ui/lab'; import React from 'react'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntities'; @@ -128,9 +133,12 @@ export const CatalogTable = ({ if (error) { return (
- - Error encountered while fetching catalog entities. {error.toString()} - + + +
); } 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: + +
+ +
+ + + ); +}; 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/cost-insights/src/types/Alert.ts b/plugins/cost-insights/src/types/Alert.ts index eb8919b71a..84c37a7c15 100644 --- a/plugins/cost-insights/src/types/Alert.ts +++ b/plugins/cost-insights/src/types/Alert.ts @@ -149,7 +149,7 @@ export const AlertSnoozeOptions: AlertSnoozeOption[] = [ label: '1 Month', }, { - duration: Duration.P3M, + duration: Duration.P90D, label: '1 Quarter', }, ]; 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/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 5fdc4781ce..4657066249 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -162,7 +162,7 @@ describe('AuditList', () => { , ), ); - const element = await rendered.findByTestId('error-message'); + const element = await rendered.findByText(/Could not load audit list./); expect(element).toBeInTheDocument(); }); }); diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index ce0fcdb46e..8d7f769909 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -17,7 +17,6 @@ import React, { useState, useMemo, ReactNode } from 'react'; import { useLocalStorage, useAsync } from 'react-use'; import { useNavigate } from 'react-router-dom'; import { Grid, Button } from '@material-ui/core'; -import Alert from '@material-ui/lab/Alert'; import Pagination from '@material-ui/lab/Pagination'; import { InfoCard, @@ -28,6 +27,7 @@ import { HeaderLabel, Progress, useApi, + WarningPanel, } from '@backstage/core'; import { lighthouseApiRef } from '../../api'; @@ -85,9 +85,9 @@ const AuditList = () => { content = ; } else if (error) { content = ( - + {error.message} - + ); } 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 252f498bd6..02f2f9e976 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", @@ -62,7 +63,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/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/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 e9239b8ea2..959e9e42c5 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -136,7 +136,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, @@ -308,3 +311,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 250a59e775..2bc7c76a3a 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, TaskPage } from '../src/plugin'; @@ -30,8 +30,9 @@ createDevApp() }) .registerApi({ api: scaffolderApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new ScaffolderClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new ScaffolderClient({ discoveryApi, identityApi }), }) .addPage({ path: '/create', diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index a97d339ba8..6f55d0114e 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,9 +31,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.1", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", - "@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", @@ -58,8 +58,9 @@ }, "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", + "@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 afb84fe173..36bac5875c 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -15,7 +15,12 @@ */ import { JsonObject } from '@backstage/config'; -import { createApiRef, DiscoveryApi, Observable } from '@backstage/core'; +import { + createApiRef, + DiscoveryApi, + Observable, + IdentityApi, +} from '@backstage/core'; import ObservableImpl from 'zen-observable'; import { ScaffolderTask, Status } from './types'; @@ -58,9 +63,14 @@ export interface ScaffolderApi { } export class ScaffolderClient implements 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; } /** @@ -74,11 +84,13 @@ export class ScaffolderClient implements ScaffolderApi { templateName: string, values: Record, ): Promise { + const token = await this.identityApi.getIdToken(); const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), }, body: JSON.stringify({ templateName, values: { ...values } }), }); @@ -93,10 +105,13 @@ export class ScaffolderClient implements ScaffolderApi { return id; } - async getTask(taskId: string): Promise { + async getTask(taskId: string) { + const token = await this.identityApi.getIdToken(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; - return fetch(url).then(x => x.json()); + return fetch(url, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }).then(x => x.json()); } streamLogs({ diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 13123e81bf..abb75355d0 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'; @@ -31,8 +32,9 @@ export const scaffolderPlugin = createPlugin({ apis: [ createApiFactory({ api: scaffolderApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new ScaffolderClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new ScaffolderClient({ 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-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts index d91e2f08a2..0fd23a4f7d 100644 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ b/plugins/techdocs-backend/src/service/standaloneServer.ts @@ -53,6 +53,7 @@ export async function startStandaloneServer( const mockUrlReader: jest.Mocked = { read: jest.fn(), readTree: jest.fn(), + search: jest.fn(), }; logger.debug('Creating application...'); 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/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index f064c35517..d1841c3bba 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -15,12 +15,14 @@ */ import { + CodeSnippet, Content, Header, ItemCard, Page, Progress, useApi, + WarningPanel, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; @@ -61,7 +63,12 @@ export const TechDocsHome = () => { subtitle="Documentation available in Backstage" /> -

{error.message}

+ + +
); 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/FeatureFlags/EmptyFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx index de6f4703e0..985a67189b 100644 --- a/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx @@ -21,7 +21,7 @@ import { Button, Typography } from '@material-ui/core'; const EXAMPLE = `import { createPlugin } from '@backstage/core'; export default createPlugin({ - id: 'welcome', + id: 'plugin-name', register({ router, featureFlags }) { featureFlags.register('enable-example-feature'); }, diff --git a/plugins/welcome/README.md b/plugins/welcome/README.md index f0211c248a..c40a760c7c 100644 --- a/plugins/welcome/README.md +++ b/plugins/welcome/README.md @@ -1,5 +1,8 @@ # Title +> This plugin formerly provided the "homepage" of early alpha versions of Backstage. +> It is no longer in active use and in most cases can be removed from your instance. + Welcome to the welcome plugin! ## Sub-section 1 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 45f8a8b4fb..1e4fafa068 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" @@ -2645,14 +1865,37 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/plugin-catalog@^0.2.0", "@backstage/plugin-catalog@^0.2.1": - version "0.3.0" +"@backstage/plugin-catalog@^0.2.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" + "@material-ui/lab" "4.0.0-alpha.45" + "@types/react" "^16.9" + classnames "^2.2.6" + git-url-parse "^11.4.4" + react "^16.13.1" + react-dom "^16.13.1" + react-helmet "6.1.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + +"@backstage/plugin-catalog@^0.2.1": + version "0.3.1" + dependencies: + "@backstage/catalog-client" "^0.3.6" + "@backstage/catalog-model" "^0.7.1" + "@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 +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" @@ -3316,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" @@ -6109,30 +5351,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" @@ -6156,13 +5379,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" @@ -6170,17 +5386,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== @@ -6576,11 +5782,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" @@ -6844,12 +6048,17 @@ "@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" "*" +"@types/humanize-duration@^3.18.1": + version "3.18.1" + resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.18.1.tgz#10090d596053703e7de0ac43a37b96cd9fc78309" + integrity sha512-MUgbY3CF7hg/a/jogixmAufLjJBQT7WEf8Q+kYJkOc47ytngg1IuZobCngdTjAgY83JWEogippge5O5fplaQlw== + "@types/inquirer@^7.3.1": version "7.3.1" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" @@ -8849,6 +8058,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" @@ -9498,11 +8722,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" @@ -9732,7 +8951,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== @@ -10425,7 +9644,7 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -12006,6 +11225,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" @@ -13175,6 +12399,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" @@ -13472,11 +12701,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" @@ -13546,11 +12770,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" @@ -13795,12 +13014,12 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== +find-versions@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" + integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== dependencies: - semver-regex "^2.0.0" + semver-regex "^3.1.2" find-yarn-workspace-root2@1.2.16: version "1.2.16" @@ -14886,7 +14105,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.4.0, handlebars@^4.7.3: +handlebars@^4.4.0, handlebars@^4.7.3, handlebars@^4.7.6: version "4.7.6" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -15398,6 +14617,11 @@ human-signals@^1.1.1: resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +humanize-duration@^3.25.1: + version "3.25.1" + resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.25.1.tgz#50e12bf4b3f515ec91106107ee981e8cfe955d6f" + integrity sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -15406,17 +14630,17 @@ humanize-ms@^1.2.1: ms "^2.0.0" husky@^4.2.3: - version "4.3.6" - resolved "https://registry.npmjs.org/husky/-/husky-4.3.6.tgz#ebd9dd8b9324aa851f1587318db4cccb7665a13c" - integrity sha512-o6UjVI8xtlWRL5395iWq9LKDyp/9TE7XMOTvIpEVzW638UcGxTmV5cfel6fsk/jbZSTlvfGVJf2svFtybcIZag== + version "4.3.8" + resolved "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" + integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow== dependencies: chalk "^4.0.0" ci-info "^2.0.0" compare-versions "^3.6.0" cosmiconfig "^7.0.0" - find-versions "^3.2.0" + find-versions "^4.0.0" opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" + pkg-dir "^5.0.0" please-upgrade-node "^3.2.0" slash "^3.0.0" which-pm-runs "^1.0.0" @@ -15452,6 +14676,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" @@ -16923,6 +16152,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" @@ -18166,7 +17400,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== @@ -20738,6 +19972,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -21296,9 +20537,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" @@ -22038,13 +21279,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" @@ -23147,10 +22381,10 @@ rollup-plugin-postcss@^3.1.1: safe-identifier "^0.4.1" style-inject "^0.3.0" -rollup-plugin-typescript2@^0.27.3: - version "0.27.3" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz#cd9455ac026d325b20c5728d2cc54a08a771b68b" - integrity sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg== +rollup-plugin-typescript2@^0.29.0: + version "0.29.0" + resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" + integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== dependencies: "@rollup/pluginutils" "^3.1.0" find-cache-dir "^3.3.1" @@ -23282,6 +22516,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" @@ -23359,10 +22598,10 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== +semver-regex@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" + integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" @@ -25360,7 +24599,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== @@ -25832,6 +25076,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" @@ -25847,6 +25099,11 @@ use-composed-ref@^1.0.0: dependencies: ts-essentials "^2.0.3" +use-immer@^0.4.2: + version "0.4.2" + resolved "https://registry.npmjs.org/use-immer/-/use-immer-0.4.2.tgz#7d7e7d3386cc834d45b2279f37d8974023550f4f" + integrity sha512-ONfZHEv/gzt/jyYxrJD3ZFUllKJED8F1mds9Fr9CYj54LmsiuGDg3vkx+R7WHS72p7DbSS1QbM7xNYrVWX+KcA== + use-isomorphic-layout-effect@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.0.0.tgz#f56b4ed633e1c21cd9fc76fe249002a1c28989fb" @@ -25930,7 +25187,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== @@ -26651,6 +25913,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" @@ -26664,6 +25934,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"