diff --git a/.changeset/afraid-trainers-know.md b/.changeset/afraid-trainers-know.md new file mode 100644 index 0000000000..5df3d3eced --- /dev/null +++ b/.changeset/afraid-trainers-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fix a bug preventing an access token to be refreshed a second time with the GitLab provider. diff --git a/.changeset/angry-ghosts-report.md b/.changeset/angry-ghosts-report.md deleted file mode 100644 index dfa83663a9..0000000000 --- a/.changeset/angry-ghosts-report.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Support a `ensureExists` config option to skip ensuring a configured database exists. This allows deployment scenarios where -limited permissions are given for provisioned databases without privileges to create new databases. If set to `false`, the -database connection will not be validated prior to use which means the backend will not attempt to create the database if it -doesn't exist. You can configure this in your app-config.yaml: - -```yaml -backend: - database: - ensureExists: false -``` - -This defaults to `true` if unspecified. You can also configure this per plugin connection and will override the base option. diff --git a/.changeset/angry-rules-fail.md b/.changeset/angry-rules-fail.md deleted file mode 100644 index cc52d4478e..0000000000 --- a/.changeset/angry-rules-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Add unstable prop for disabling unregister entity menu diff --git a/.changeset/brave-eggs-rush.md b/.changeset/brave-eggs-rush.md new file mode 100644 index 0000000000..8e1a7ca86d --- /dev/null +++ b/.changeset/brave-eggs-rush.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +--- + +Change the styling of the `` to have more contrast in light +mode. Nodes now have a design similar to material UI buttons. diff --git a/.changeset/cuddly-rabbits-look.md b/.changeset/cuddly-rabbits-look.md new file mode 100644 index 0000000000..421c97bbff --- /dev/null +++ b/.changeset/cuddly-rabbits-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Action needed: If you are using the templates located at https://github.com/backstage/backstage/tree/master/ in your Backstage app directly using the URL via the `app-config.yaml`, you should copy over the templates inside your org and import from there. The templates have now been moved to https://github.com/backstage/software-templates. See https://github.com/backstage/backstage/issues/6415 for explanation. diff --git a/.changeset/curly-badgers-sit.md b/.changeset/curly-badgers-sit.md deleted file mode 100644 index ca740e907a..0000000000 --- a/.changeset/curly-badgers-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Store filter values set in `EntityListProvider` in query parameters. This allows selected filters to be restored when returning to pages that list catalog entities. diff --git a/.changeset/curvy-timers-sit.md b/.changeset/curvy-timers-sit.md new file mode 100644 index 0000000000..346fae9098 --- /dev/null +++ b/.changeset/curvy-timers-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +replace the deprecated file-loader for fonts with assets module diff --git a/.changeset/fast-actors-grow.md b/.changeset/fast-actors-grow.md new file mode 100644 index 0000000000..c1da84f744 --- /dev/null +++ b/.changeset/fast-actors-grow.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Bump `passport-saml` to version 3. This is a breaking change, in that it [now requires](https://github.com/node-saml/passport-saml/pull/548) the `auth.saml.cert` parameter to be set. If you are not using SAML auth, you can ignore this. + +To update your settings, add something similar to the following to your app-config: + +```yaml +auth: + saml: + # ... other settings ... + cert: 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W==' +``` + +For more information, see the [library README](https://github.com/node-saml/passport-saml#security-and-signatures). diff --git a/.changeset/gorgeous-pugs-deliver.md b/.changeset/gorgeous-pugs-deliver.md new file mode 100644 index 0000000000..82820baa67 --- /dev/null +++ b/.changeset/gorgeous-pugs-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Set the default TZ (Timezone) env for the test command to be UTC so any date related tests are consistent across timezones. diff --git a/.changeset/green-vans-peel.md b/.changeset/green-vans-peel.md deleted file mode 100644 index cd4d30139a..0000000000 --- a/.changeset/green-vans-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Fix `EntityTypeFilter` so it produces unique case-insensitive set of available types diff --git a/.changeset/grumpy-beds-design.md b/.changeset/grumpy-beds-design.md new file mode 100644 index 0000000000..afca4e0eb3 --- /dev/null +++ b/.changeset/grumpy-beds-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Fix import for `createPlugin` in example snippet diff --git a/.changeset/grumpy-dolls-call.md b/.changeset/grumpy-dolls-call.md deleted file mode 100644 index 13d96ecb1f..0000000000 --- a/.changeset/grumpy-dolls-call.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-catalog-react': minor -'@backstage/plugin-scaffolder': patch ---- - -Updated the software templates list page (`ScaffolderPage`) to use the `useEntityListProvider` hook from #5643. This reduces the code footprint, making it easier to customize the display of this page, and consolidates duplicate approaches to querying the catalog with filters. - -- The `useEntityTypeFilter` hook has been updated along with the underlying `EntityTypeFilter` to work with multiple values, to allow more flexibility for different user interfaces. It's unlikely that this change affects you; however, if you're using either of these directly, you'll need to update your usage. -- `SearchToolbar` was renamed to `EntitySearchBar` and moved to `catalog-react` to be usable by other entity list pages -- `UserListPicker` now has an `availableTypes` prop to restrict which user-related options to present diff --git a/.changeset/hot-otters-taste.md b/.changeset/hot-otters-taste.md new file mode 100644 index 0000000000..1e41f980f1 --- /dev/null +++ b/.changeset/hot-otters-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic': patch +--- + +Added pagination and searching to the table diff --git a/.changeset/late-mangos-allow.md b/.changeset/late-mangos-allow.md new file mode 100644 index 0000000000..e1e4fe1d56 --- /dev/null +++ b/.changeset/late-mangos-allow.md @@ -0,0 +1,28 @@ +--- +'@backstage/plugin-home': minor +--- + +The homepage `
` is now part of the composable canvas (allowing you to add the , for example). + +You will need to wrap your existing composed `` component in ``, `
`, and `` components, like this: + +```diff +// app/src/components/home/HomePage.tsx + ++ import { Content, Header, Page, HomePageTimer } from '@backstage/core-components'; + +export const HomePage = () => ( ++ ++
++ ++
++ + + + + + // ... ++ ++
+); +``` diff --git a/.changeset/lemon-crabs-confess.md b/.changeset/lemon-crabs-confess.md deleted file mode 100644 index 0d87843df5..0000000000 --- a/.changeset/lemon-crabs-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Updating fs-extra to 10.0.0 to handle broken symbolic links correctly diff --git a/.changeset/lemon-dancers-taste.md b/.changeset/lemon-dancers-taste.md deleted file mode 100644 index 0542fe4a54..0000000000 --- a/.changeset/lemon-dancers-taste.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -The codeowners processor extracts the username of the primary owner and uses this as the owner field. -Given the kind isn't specified this is assumed to be a group and so the link to the owner in the about card -doesn't work. This change specifies the kind where the entity is a user. e.g: - -`@iain-b` -> `user:iain-b` diff --git a/.changeset/neat-mugs-add.md b/.changeset/neat-mugs-add.md new file mode 100644 index 0000000000..b90c95440d --- /dev/null +++ b/.changeset/neat-mugs-add.md @@ -0,0 +1,7 @@ +--- +'@backstage/integration': patch +--- + +Fix Azure URL handling to properly support both repo shorthand (`/owner/_git/project`) and full URLs (`/owner/project/_git/repo`). + +Fix Azure DevOps Server URL handling by being able to parse URLs with hosts other than `dev.azure.com`. Note that the `api-version` used for API requests is currently `6.0`, meaning you need to support at least this version in your Azure DevOps Server instance. diff --git a/.changeset/nice-bugs-beg.md b/.changeset/nice-bugs-beg.md deleted file mode 100644 index c41b54b220..0000000000 --- a/.changeset/nice-bugs-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': minor ---- - -Exported and renamed components from the `@backstage/plugin-user-settings` plugin , to be able to use it in the consumer side and customize the `SettingPage` diff --git a/.changeset/perfect-keys-hide.md b/.changeset/perfect-keys-hide.md new file mode 100644 index 0000000000..cab9967363 --- /dev/null +++ b/.changeset/perfect-keys-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fill in most missing type exports. diff --git a/.changeset/polite-dragons-allow.md b/.changeset/polite-dragons-allow.md new file mode 100644 index 0000000000..05eac53a1f --- /dev/null +++ b/.changeset/polite-dragons-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Use a more informative error message if the configured OIDC identity provider does not provide a `userinfo_endpoint` in its metadata. diff --git a/.changeset/polite-spies-judge.md b/.changeset/polite-spies-judge.md deleted file mode 100644 index 075bcf1598..0000000000 --- a/.changeset/polite-spies-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-rails': patch ---- - -updated paths to consider differences between platform (windows corrected) diff --git a/.changeset/poor-otters-buy.md b/.changeset/poor-otters-buy.md deleted file mode 100644 index b60cbddc1b..0000000000 --- a/.changeset/poor-otters-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Add new `fetch:template` action which handles the same responsibilities as `fetch:cookiecutter` without the external dependency on `cookiecutter`. For information on migrating from `fetch:cookiecutter` to `fetch:template`, see the [migration guide](https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetch-cookiecutter-to-fetch-template) in the docs. diff --git a/.changeset/poor-weeks-act.md b/.changeset/poor-weeks-act.md new file mode 100644 index 0000000000..152ce4bfc6 --- /dev/null +++ b/.changeset/poor-weeks-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Change the Categories filter to a dropdown component diff --git a/.changeset/pretty-drinks-serve.md b/.changeset/pretty-drinks-serve.md deleted file mode 100644 index a012aba11f..0000000000 --- a/.changeset/pretty-drinks-serve.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Scaffolder: Added an 'eq' handlebars helper for use in software template YAML files. This can be used to execute a step depending on the value of an input, e.g.: - -```yaml -steps: - id: 'conditional-step' - action: 'custom-action' - if: '{{ eq parameters.myvalue "custom" }}', -``` diff --git a/.changeset/real-beans-collect.md b/.changeset/real-beans-collect.md new file mode 100644 index 0000000000..43dae9cd9a --- /dev/null +++ b/.changeset/real-beans-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Fix Azure `readTree` and `search` handling to properly support paths. diff --git a/.changeset/real-plums-vanish.md b/.changeset/real-plums-vanish.md deleted file mode 100644 index a653a6bdf2..0000000000 --- a/.changeset/real-plums-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Add Sign In and Handler resolver for Microsoft provider diff --git a/.changeset/rotten-pears-live.md b/.changeset/rotten-pears-live.md new file mode 100644 index 0000000000..4353341c2e --- /dev/null +++ b/.changeset/rotten-pears-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Added rejectUnauthorized config option diff --git a/.changeset/search-mighty-mice-collect.md b/.changeset/search-mighty-mice-collect.md deleted file mode 100644 index bad91ff853..0000000000 --- a/.changeset/search-mighty-mice-collect.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': minor ---- - -Change return value of `SearchEngine.index` to `Promise` to support -implementation of external search engines. diff --git a/.changeset/short-eggs-confess.md b/.changeset/short-eggs-confess.md deleted file mode 100644 index 3fa240a347..0000000000 --- a/.changeset/short-eggs-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-ldap': patch ---- - -Expose missing types used by the custom transformers diff --git a/.changeset/short-mugs-drive.md b/.changeset/short-mugs-drive.md new file mode 100644 index 0000000000..f2b34fd2c4 --- /dev/null +++ b/.changeset/short-mugs-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Improve UX for Login pop-up diff --git a/.changeset/shy-rules-design.md b/.changeset/shy-rules-design.md deleted file mode 100644 index 4f009e5aa8..0000000000 --- a/.changeset/shy-rules-design.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Updated inputs for the `publish:github:pull-request` action. - -Now requires a `repoUrl` instead of separate `owner` and `repo` inputs. This aligns with the output of the `RepoUrlPicker` ui field used by the pull-request sample template. diff --git a/.changeset/silly-trainers-clean.md b/.changeset/silly-trainers-clean.md new file mode 100644 index 0000000000..b4690a0e8f --- /dev/null +++ b/.changeset/silly-trainers-clean.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +--- + +Switch to using utilities from `@backstage/version-bridge'. diff --git a/.changeset/six-crabs-sit.md b/.changeset/six-crabs-sit.md new file mode 100644 index 0000000000..9389906ee9 --- /dev/null +++ b/.changeset/six-crabs-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +remove hard coded min height in page header diff --git a/.changeset/strong-geese-hope.md b/.changeset/strong-geese-hope.md new file mode 100644 index 0000000000..27da35743a --- /dev/null +++ b/.changeset/strong-geese-hope.md @@ -0,0 +1,13 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs': patch +--- + +Upgrade git-parse-url to v11.6.0 diff --git a/.changeset/techdocs-small-bugs-exist.md b/.changeset/techdocs-small-bugs-exist.md new file mode 100644 index 0000000000..5e7c01c1a4 --- /dev/null +++ b/.changeset/techdocs-small-bugs-exist.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-home': patch +--- + +Adjust dependencies to `@types/react` and `react-router` to follow the pattern +used by all other Backstage packages. diff --git a/.changeset/techdocs-typescript-isnt-fun.md b/.changeset/techdocs-typescript-isnt-fun.md deleted file mode 100644 index 9341c1c14a..0000000000 --- a/.changeset/techdocs-typescript-isnt-fun.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch ---- - -It is no longer required to provide a generator and a preparer to the TechDocs -router factory when running TechDocs in the "recommended" (e.g. externally -prepared and generated docs) configuration. diff --git a/.changeset/ten-dolls-ring.md b/.changeset/ten-dolls-ring.md new file mode 100644 index 0000000000..8142d1f3c8 --- /dev/null +++ b/.changeset/ten-dolls-ring.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-explore': patch +--- + +Make styling of groups tab in the explore page use the Backstage theme instead +of hard coded colors. Change the layout to be full screen. diff --git a/.changeset/tidy-shoes-occur.md b/.changeset/tidy-shoes-occur.md new file mode 100644 index 0000000000..9ffac60f56 --- /dev/null +++ b/.changeset/tidy-shoes-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Handle pagination on return results when fetch GitHub API to list selected repos diff --git a/.changeset/unlucky-tables-repair.md b/.changeset/unlucky-tables-repair.md new file mode 100644 index 0000000000..ed1222b6d6 --- /dev/null +++ b/.changeset/unlucky-tables-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Display entity titles on `EntityLayout` if defined diff --git a/.changeset/wicked-jars-move.md b/.changeset/wicked-jars-move.md new file mode 100644 index 0000000000..d2e9bc5446 --- /dev/null +++ b/.changeset/wicked-jars-move.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-cloudbuild': patch +--- + +- Swaps over the plugin CloudBuild from using MomentJS to using Luxon for date formatting. +- Fixes some React warnings generated by nested

elements. diff --git a/.changeset/wise-rockets-smoke.md b/.changeset/wise-rockets-smoke.md deleted file mode 100644 index c81d20519d..0000000000 --- a/.changeset/wise-rockets-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Document the default behavior of `statusCheck` option in `createStatusCheckRouter`. diff --git a/.changeset/young-tables-reply.md b/.changeset/young-tables-reply.md deleted file mode 100644 index e08a875c14..0000000000 --- a/.changeset/young-tables-reply.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/cli': patch -'@backstage/config-loader': patch -'@backstage/create-app': patch -'@backstage/techdocs-common': patch -'@backstage/plugin-app-backend': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-rollbar-backend': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fda500ed02..0854635a2b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,17 +8,18 @@ /docs/features/techdocs @backstage/techdocs-core /docs/features/search @backstage/techdocs-core /docs/assets/search @backstage/techdocs-core -/plugins/code-coverage @alde @nissayeva -/plugins/code-coverage-backend @alde @nissayeva +/plugins/code-coverage @backstage/reviewers @alde @nissayeva +/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva /plugins/cost-insights @backstage/silver-lining -/plugins/cloudbuild @trivago/ebarrios +/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios /plugins/search @backstage/techdocs-core /plugins/search-* @backstage/techdocs-core /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core -/plugins/ilert @yacut +/plugins/ilert @backstage/reviewers @yacut +/plugins/home @backstage/techdocs-core /packages/search-common @backstage/techdocs-core /packages/techdocs-common @backstage/techdocs-core -/.changeset/cost-insights-* @backstage/silver-lining +/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining /.changeset/search-* @backstage/techdocs-core /.changeset/techdocs-* @backstage/techdocs-core diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index e6af881f1e..5c9d46ea8a 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -1,5 +1,6 @@ abc accessors +Anddddd Apdex api Api @@ -14,6 +15,7 @@ Avro backrub Bigtable Billett +Bitbucket Bitrise Blackbox bool @@ -66,7 +68,6 @@ devs discoverability Discoverability dls -docgen Dockerfile dockerfiles Dockerize @@ -74,6 +75,7 @@ dockerode Docusaurus env Env +elasticsearch esbuild eslint etag @@ -82,6 +84,8 @@ facto failover Fargate Figma +firehydrant +FireHydrant Firekube Fiverr gitbeaker @@ -137,6 +141,7 @@ maintainership makefile md memcache +memoize memoized microservice microservices @@ -210,8 +215,10 @@ repo Repo repos rerender +rerenders Reusability reusability +roadmaps rollbar Rollbar Rollup @@ -220,6 +227,7 @@ Routable rst rsync ruleset +runbook sam sanitization scaffolded diff --git a/.github/workflows/chromatic-storybook-test.yml b/.github/workflows/chromatic-storybook-test.yml index d189236987..4c64db309d 100644 --- a/.github/workflows/chromatic-storybook-test.yml +++ b/.github/workflows/chromatic-storybook-test.yml @@ -5,6 +5,7 @@ on: - '.github/workflows/chromatic-storybook-test.yml' - 'packages/storybook/**' - 'packages/core-components/src/**' + - '**/*.stories.tsx' jobs: chromatic: @@ -50,4 +51,5 @@ jobs: # projetToken intentionally shared to allow collaborators to run Chromatic on forks # https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects projectToken: 9tzak77m9nj - storybookBuildDir: 'packages/storybook/dist' + workingDir: 'packages/storybook' + storybookBuildDir: 'dist' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f013d10f8..d203fafa2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,11 +105,8 @@ jobs: run: git diff --quiet origin/master HEAD -- yarn.lock continue-on-error: true - - name: verify doc links - run: node scripts/verify-links.js - - name: prettier - run: yarn prettier:check + run: yarn prettier:check '!ADOPTERS.md' - name: lock run: yarn lock:check @@ -123,8 +120,12 @@ jobs: - name: type checking and declarations run: yarn tsc:full - - name: check api reports - run: yarn build:api-reports:only --ci + # We need to generate the API references as well, so that we can verify the doc links + - name: check api reports and generate API reference + run: yarn build:api-reports:only --ci --docs + + - name: verify doc links + run: node scripts/verify-links.js - name: build changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 1844700cea..3e2fbe0863 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -25,6 +25,13 @@ jobs: name: Node ${{ matrix.node-version }} on ${{ matrix.os }} steps: + # In order to have the create-app template function as if it was downloaded from NPM + # we need to make sure we checkout files with LF line endings only + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@v2 - name: use node.js ${{ matrix.node-version }} diff --git a/.github/workflows/microsite-build-check.yml b/.github/workflows/microsite-build-check.yml index 68a6c08cd9..73f63ed63a 100644 --- a/.github/workflows/microsite-build-check.yml +++ b/.github/workflows/microsite-build-check.yml @@ -27,9 +27,6 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: verify doc links - run: node scripts/verify-links.js - # Skip caching of microsite dependencies, it keeps the global cache size # smaller, which make Windows builds a lot faster for the rest of the project. - name: yarn install diff --git a/.github/workflows/microsite-with-storybook-deploy.yml b/.github/workflows/microsite-with-storybook-deploy.yml index e8886b7d6b..47e18340eb 100644 --- a/.github/workflows/microsite-with-storybook-deploy.yml +++ b/.github/workflows/microsite-with-storybook-deploy.yml @@ -4,12 +4,6 @@ on: push: branches: - master - paths: - - '.github/workflows/microsite-with-storybook-deploy.yml' - - 'packages/storybook/**' - - 'packages/core-components/src/**' - - 'microsite/**' - - 'docs/**' jobs: deploy-microsite-and-storybook: @@ -41,6 +35,9 @@ jobs: run: yarn install --frozen-lockfile working-directory: microsite + - name: build API reference + run: yarn build:api-docs + - name: build microsite run: yarn build working-directory: microsite diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml new file mode 100644 index 0000000000..4a34ff0c88 --- /dev/null +++ b/.github/workflows/prettify.yml @@ -0,0 +1,54 @@ +name: Prettier + +on: + push: + branches: + - master + +jobs: + autofix-markdown: + name: Autofix Markdown files using Prettier + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + # Fetch changes to previous commit - required for 'only_changed' in Prettier action + fetch-depth: 0 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: Run Prettier on ADOPTERS.md + uses: creyD/prettier_action@v3.1 + with: + # Modifies commit only if prettier autofixed the ADOPTERS.md + prettier_options: --config docs/prettier.config.js --write ADOPTERS.md + only_changed: true + commit_message: 'Autofix ADOPTERS.md using Prettier' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/snyk-monitor.yml b/.github/workflows/snyk-monitor.yml new file mode 100644 index 0000000000..0adb6c5bcb --- /dev/null +++ b/.github/workflows/snyk-monitor.yml @@ -0,0 +1,51 @@ +name: Snyk Monitoring + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - '.github/workflows/snyk-monitor.yml' + - '**/.snyk' + - '**/package.json' + - 'yarn.lock' + +# This workflow synchronizes the packages in this repo along with policies in +# each .snyk file with the remote state in our snyk org. It allows us to define +# ignore policies in the .snyk files and then have them show up in the snyk web +# UI, and also automatically adds any new packages that are created. + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Monitor and Synchronize Snyk Policies + uses: snyk/actions/node@master + with: + command: monitor + args: > + --yarn-workspaces + --org=backstage-dgh + --strict-out-of-sync=false + --remote-repo-url=https://github.com/backstage/backstage + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + + # Above we run the `monitor` command, this runs the `test` command which is + # the one that generates the SARIF report that we can upload to GitHub. + - name: Create Snyk report + uses: snyk/actions/node@master + continue-on-error: true # To make sure that SARIF upload gets called + with: + args: > + --yarn-workspaces + --org=backstage-dgh + --strict-out-of-sync=false + --sarif-file-output=snyk.sarif + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Upload Snyk report + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: snyk.sarif diff --git a/.imgbotconfig b/.imgbotconfig index f337e087dc..2f4af2ed5e 100644 --- a/.imgbotconfig +++ b/.imgbotconfig @@ -1,5 +1,3 @@ { - "ignoredFiles": [ - "docs/assets/**/*.svg" - ] + "ignoredFiles": ["docs/assets/**/*.svg"] } diff --git a/ADOPTERS.md b/ADOPTERS.md index 1dcd4a774f..18d1388936 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,39 +1,47 @@ -| Organization | Contact | Description of Use | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| Organization | Contact | Description of Use | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2caa8c9c9c..466c77c575 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,7 +116,7 @@ In general, changesets are not needed for the documentation, build utilities, co 1. Run `yarn changeset` 2. Select which packages you want to include a changeset for -3. Select impact of change that you're introducing (patch, minor, or major) +3. Select impact of change that you're introducing, using `minor` for breaking changes and `patch` otherwise. We do not use `major` changes while packages are at version `0.x`. 4. Add generated changeset to Git 5. Push the commit with your changeset to the branch associated with your PR 6. Accept our gratitude for making the release process easier on the maintainers diff --git a/README.md b/README.md index 98da194c1b..1078631926 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ # [Backstage](https://backstage.io) -_During the month of July the majority of the maintainers will be on summer vacation 🏖️ Development will continue as usual, but expect a slower pace for discussions and PR reviews. Why not take this opportunity to [build a plugin](https://backstage.io/docs/plugins/)?_ - [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects) [![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22) @@ -63,3 +61,9 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how Copyright 2020-2021 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +## Security + +Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. + +For further details please see our complete [security release process](SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md index 3ec51b4210..ba96c694ee 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,3 +9,30 @@ ## Reporting a Vulnerability Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. + +If you have questions about a potential vulnerability, please reach out on Discord by asking for a maintainer in the `#support` channel, or via direct message to a maintainer. + +## Remediation and Notification Process + +Vulnerabilities are handled and published through [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories/about-github-security-advisories). + +In the event of a vulnerability the runbook for the maintainers is as follows: + +1. Create a [new draft security advisory](https://github.com/backstage/backstage/security/advisories/new). The values and descriptions don't need to be perfect to begin with as they can be edited later. For severity, use the "Assess severity using CVSS" and refer to [the guide](https://www.first.org/cvss/v3.1/user-guide) for help. +2. Request a CVE identification number. It can take up to 72h for one to be assigned so be sure to do this early on. +3. If there is not already a patch for the vulnerability, collaborate on one in a private fork: + 1. Head to the security advisory on GitHub and [create a private fork](https://docs.github.com/en/code-security/security-advisories/collaborating-in-a-temporary-private-fork-to-resolve-a-security-vulnerability) + 1. [Invite any collaborators](https://docs.github.com/en/code-security/security-advisories/adding-a-collaborator-to-a-security-advisory) from outside the maintainer team that should be part of creating a fix. + 1. Create one or multiple Pull Requests with fixes towards the fork. Note that these PRs will not have CI checks run on them, so more care should be taken to run local validation. The PRs are also not merged like normal PRs, but are instead merged straight into the main repo all at once when the merge button is hit on the security advisory. +4. Once the fix is ready in a PR or private fork and it is time to release it, there are a couple of options. Either merge into the main branch with a changeset and wait for a regular release, or do a quick release by manually bumping the version in `package.json` of the affected package, along with a manual `CHANGELOG.md` entry. Note that a quick release will only work if the package does not have any other pending changes that depend on pending changes in other packages, so be sure to manually check that first, and fall back to an early regular release if needed. In general it's best to stick with the regular release flow, with the quick release being used only for time sensitive fixes. +5. Finalize and publish the security advisory. Note that once you hit the publish button it's no longer possible to edit the advisory. Just like the CVE number this can take up to 72h, and expect it to be slower than the CVE number request. + +## Vulnerability Policies + +Backstage uses Snyk vulnerability scans in order to make sure we minimize vulnerabilities in our dependencies and get notified of new vulnerabilities. + +There are many situations where a vulnerability does not affect a particular dependency because of how the vulnerable package is used. In that situation the package authors may choose to stay at the current version rather than bumping the dependency, leading to a warning in the vulnerability scans but no actual vulnerability. + +To work around this and other similar issues, Snyk provides a method to ignore vulnerabilities. In order to provide the best visibility and most utility to adopters of Backstage, we store these ignore rules in `.snyk` policy files. This allows adopters to rely on our ignore policies if they wish to do so. + +Adding a new ignore policy is done by creating or modifying an existing `.snyk` file within a package root. See the [Snyk Documentation](https://support.snyk.io/hc/en-us/articles/360007487097-The-snyk-file) for details on the syntax. Always include a description, full path, and time limit of the ignore policy. diff --git a/app-config.yaml b/app-config.yaml index b50de963ed..2fb75f718e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -101,8 +101,10 @@ organization: # 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: - techdocs: 'docker' # Alternatives - 'local' + generator: + runIn: 'docker' + # dockerImage: my-org/techdocs # use a custom docker image + # pullImage: true # or false to disable automatic pulling of image (e.g. if custom docker login is required) publisher: type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives. @@ -131,6 +133,9 @@ kafka: brokers: - localhost:9092 +allure: + baseUrl: http://localhost:5050/allure-docker-service + integrations: github: - host: github.com @@ -150,12 +155,21 @@ integrations: - host: bitbucket.org username: ${BITBUCKET_USERNAME} appPassword: ${BITBUCKET_APP_PASSWORD} + ### Example for how to add your bitbucket server instance using the API: + # - host: server.bitbucket.com + # apiBaseUrl: server.bitbucket.com + # username: ${BITBUCKET_SERVER_USERNAME} + # appPassword: ${BITBUCKET_SERVER_APP_PASSWORD} azure: - host: dev.azure.com token: ${AZURE_TOKEN} -# googleGcs: -# clientEmail: 'example@example.com' -# privateKey: ${GCS_PRIVATE_KEY} + # googleGcs: + # clientEmail: 'example@example.com' + # privateKey: ${GCS_PRIVATE_KEY} + awsS3: + - host: amazonaws.com + accessKeyId: ${AWS_ACCESS_KEY_ID} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY} catalog: rules: @@ -254,6 +268,8 @@ scaffolder: # defaultAuthor: # name: Scaffolder # email: scaffolder@backstage.io + # Use to customize the default commit message when new components are created + # defaultCommitMessage: 'Initial commit' github: token: ${GITHUB_TOKEN} visibility: public # or 'internal' or 'private' @@ -296,6 +312,7 @@ auth: saml: entryPoint: 'http://localhost:7001/' issuer: 'passport-saml' + cert: 'fake-cert-base64' okta: development: clientId: ${AUTH_OKTA_CLIENT_ID} @@ -312,13 +329,20 @@ auth: # # scope: saml-login-selector openid profile email oidc: + # Note that you must define a session secret (see above) since the oidc provider requires session support. + # Note that by default, this provider will use the 'none' prompt which assumes that your are already logged on in the IDP. + # You should set prompt to: + # - auto: will let the IDP decide if you need to log on or if you can skip login when you have an active SSO session + # - login: will force the IDP to always present a login form to the user development: metadataUrl: ${AUTH_OIDC_METADATA_URL} clientId: ${AUTH_OIDC_CLIENT_ID} clientSecret: ${AUTH_OIDC_CLIENT_SECRET} authorizationUrl: ${AUTH_OIDC_AUTH_URL} tokenUrl: ${AUTH_OIDC_TOKEN_URL} - tokenSignedResponseAlg: ${AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG} + tokenSignedResponseAlg: ${AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG} # default='RS256' + scope: ${AUTH_OIDC_SCOPE} # default='openid profile email' + prompt: ${AUTH_OIDC_PROMPT} # default=none (allowed values: auto, none, consent, login) auth0: development: clientId: ${AUTH_AUTH0_CLIENT_ID} @@ -370,3 +394,9 @@ homepage: timezone: 'Asia/Tokyo' pagerduty: eventsBaseUrl: 'https://events.pagerduty.com/v2' +jenkins: + instances: + - name: default + baseUrl: https://jenkins.example.com + username: backstage-bot + apiKey: 123456789abcdef0123456789abcedf012 diff --git a/catalog-info.yaml b/catalog-info.yaml index 0cab0c558a..6de6fd3426 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -15,7 +15,7 @@ metadata: url: https://discord.com/invite/EBHEGzX annotations: github.com/project-slug: backstage/backstage - backstage.io/techdocs-ref: url:https://github.com/backstage/backstage + backstage.io/techdocs-ref: dir:. lighthouse.com/website-url: https://backstage.io spec: type: library diff --git a/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild b/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild index 1e0134017f..f0fa4a034b 100644 --- a/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild +++ b/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild @@ -8,7 +8,6 @@ # simply copies in the build output into the image. If you want to also perform # the build itself inside docker, use Dockerfile.build in this folder instead. - # USAGE: # # - Copy this file and the "docker" folder from this directory to your project @@ -23,7 +22,7 @@ # # yarn install # yarn tsc -# yarn build --config --config ... +# yarn workspace app build --config --config ... # docker build -t backstage-frontend -f Dockerfile.hostbuild . @@ -38,4 +37,3 @@ COPY docker/default.conf.template /etc/nginx/templates/default.conf.template COPY docker/inject-config.sh /docker-entrypoint.d/40-inject-config.sh ENV PORT 80 - diff --git a/contrib/docs/tutorials/aws-deployment.md b/contrib/docs/tutorials/aws-deployment.md index 47170658a3..3a76da2ed3 100644 --- a/contrib/docs/tutorials/aws-deployment.md +++ b/contrib/docs/tutorials/aws-deployment.md @@ -5,7 +5,7 @@ image](https://backstage.io/docs/getting-started/deployment-docker); this tutorial shows how to deploy that Docker image to AWS using Elastic Container Registry (ECR) and Elastic Kubernetes Service (EKS). Amazon also supports deployments with Helm, covered in the [Helm -Kubernetes](../kubernetes/basic_kubernetes_example_with_helm) example. +Kubernetes](../../kubernetes/basic_kubernetes_example_with_helm) example. The basic workflow for this method is to build a Backstage Docker image, upload the new version to a container registry, and update a Kubernetes deployment with diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..90b1e2c7ef --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +# This is generated by build:api-docs in the root +reference diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index f04202b79b..1b364bcdae 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -12,25 +12,32 @@ however always be a need for plugins to communicate outside of its boundaries, both with other plugins and the app itself. Backstage provides two primary methods for plugins to communicate across their -boundaries in client-side code. The first one being the `createPlugin` API and -the registration hooks passed to the `register` method, and the second one being -Utility APIs. While the `createPlugin` API is focused on the initialization -plugins and the app, the Utility APIs provide ways for plugins to communicate -during their entire life cycle. +boundaries in client-side code. The first one being the +[createPlugin](../reference/core-plugin-api.createplugin.md) API along with the +extensions that it can provide, and the second one being Utility APIs. While the +[createPlugin](../reference/core-plugin-api.createplugin.md) API is focused on +the initialization plugins and the app, the Utility APIs provide ways for +plugins to communicate during their entire life cycle. ## Consuming APIs -Each Utility API is tied to an `ApiRef` instance, which is a global singleton -object without any additional state or functionality, its only purpose is to -reference Utility APIs. `ApiRef`s are created using `createApiRef`, which is -exported by `@backstage/core-plugin-api`. There are many -[predefined Utility APIs](../reference/utility-apis/README.md) defined in -`@backstage/core-plugin-api`, and they're all exported with a name of the -pattern `*ApiRef`, for example `errorApiRef`. +Each Utility API is tied to an [ApiRef](../reference/core-plugin-api.apiref.md) +instance, which is a global singleton object without any additional state or +functionality, its only purpose is to reference Utility APIs. +[ApiRef](../reference/core-plugin-api.apiref.md)s are created using +[createApiRef](../reference/core-plugin-api.createapiref.md), which is exported +by [@backstage/core-plugin-api](../reference/core-plugin-api.md). There are also +many predefined Utility APIs in +[@backstage/core-plugin-api](../reference/core-plugin-api.md), and they're all +exported with a name of the pattern `*ApiRef`, for example +[errorApiRef](../reference/core-plugin-api.errorapiref.md). -To access one of the Utility APIs inside a React component, use the `useApi` -hook exported by `@backstage/core-plugin-api`, or the `withApis` HOC if you -prefer class components. For example, the `ErrorApi` can be accessed like this: +To access one of the Utility APIs inside a React component, use the +[useApi](../reference/core-plugin-api.useapi.md) hook exported by +[@backstage/core-plugin-api](../reference/core-plugin-api.md), or the +[withApis](../reference/core-plugin-api.withapis.md) HOC if you prefer class +components. For example, the +[ErrorApi](../reference/core-plugin-api.errorapi.md) can be accessed like this: ```tsx import React from 'react'; @@ -48,24 +55,31 @@ export const MyComponent = () => { }; ``` -Note that there is no explicit type given for `ErrorApi`. This is because the -`errorApiRef` has the type embedded, and `useApi` is able to infer the type. +Note that there is no explicit type given for +[ErrorApi](../reference/core-plugin-api.errorapi.md). This is because the +[errorApiRef](../reference/core-plugin-api.errorapiref.md) has the type +embedded, and [useApi](../reference/core-plugin-api.useapi.md) is able to infer +the type. Also note that consuming Utility APIs is not limited to plugins, it can be done from any component inside Backstage, including the ones in -`@backstage/core-plugin-api`. The only requirement is that they are beneath the -`AppProvider` in the react tree. +[@backstage/core-plugin-api](../reference/core-plugin-api.md). The only +requirement is that they are beneath the `AppProvider` in the react tree. ## Supplying APIs ### API Factories -APIs are registered in the form of `ApiFactories`, which encapsulate the process -of instantiating an API. It is a collection of three things: the `ApiRef` of the -API to instantiate, a list of all required dependencies, and a factory function -that returns a new API instance. +APIs are registered in the form of +[ApiFactories](../reference/core-plugin-api.apifactory.md), which encapsulate +the process of instantiating an API. It is a collection of three things: the +[ApiRef](../reference/core-plugin-api.apiref.md) of the API to instantiate, a +list of all required dependencies, and a factory function that returns a new API +instance. -For example, this is the default `ApiFactory` for the `ErrorApi`: +For example, this is the default +[ApiFactory](../reference/core-plugin-api.apifactory.md) for the +[ErrorApi](../reference/core-plugin-api.errorapi.md): ```ts createApiFactory({ @@ -79,18 +93,25 @@ createApiFactory({ }); ``` -In this example the `errorApiRef` is our API, which encapsulates the `ErrorApi` -type. The `alertApiRef` is our single dependency, which we give the name -`alertApi`, and is then passed on to the factory function, which returns an -implementation of the `ErrorApi`. +In this example the [errorApiRef](../reference/core-plugin-api.errorapiref.md) +is our API, which encapsulates the +[ErrorApi](../reference/core-plugin-api.errorapi.md) type. The +[alertApiRef](../reference/core-plugin-api.alertapiref.md) is our single +dependency, which we give the name `alertApi`, and is then passed on to the +factory function, which returns an implementation of the +[ErrorApi](../reference/core-plugin-api.errorapi.md). -The `createApiFactory` function is a thin wrapper that enables TypeScript type -inference. You may notice that there are no type annotations in the above -example, and that is because we're able to infer all types from the `ApiRef`s. -TypeScript will make sure that the return value of the `factory` function -matches the type embedded in `api`'s `ApiRef`, in this case the `ErrorApi`. It -will also match the types between the `deps` and the parameters of the `factory` -function, again using the type embedded within the `ApiRef`s. +The [createApiFactory](../reference/core-plugin-api.createapifactory.md) +function is a thin wrapper that enables TypeScript type inference. You may +notice that there are no type annotations in the above example, and that is +because we're able to infer all types from the +[ApiRef](../reference/core-plugin-api.apiref.md)s. TypeScript will make sure +that the return value of the `factory` function matches the type embedded in +`api`'s [ApiRef](../reference/core-plugin-api.apiref.md), in this case the +[ErrorApi](../reference/core-plugin-api.errorapi.md). It will also match the +types between the `deps` and the parameters of the `factory` function, again +using the type embedded within the +[ApiRef](../reference/core-plugin-api.apiref.md)s. ## Registering API Factories @@ -102,24 +123,27 @@ app, and the app itself. Starting with the Backstage core library, it provides implementations for all of the core APIs. The core APIs are the ones exported by -`@backstage/core-plugin-api`, such as the `errorApiRef` and `configApiRef`. You -can find a full list of them [here](../reference/utility-apis/README.md). +[@backstage/core-plugin-api](../reference/core-plugin-api.md), such as the +[errorApiRef](../reference/core-plugin-api.errorapiref.md) and +[configApiRef](../reference/core-plugin-api.configapiref.md). -The core APIs are loaded for any app created with `createApp` from -`@backstage/core-plugin-api`, which means that there is no step that needs to be -taken to include these APIs in an app. +The core APIs are loaded for any app created with +[createApp](../reference/core-app-api.createapp.md) from +[@backstage/core-plugin-api](../reference/core-plugin-api.md), which means that +there is no step that needs to be taken to include these APIs in an app. ### Plugin APIs In addition to the core APIs, plugins can define and export their own APIs. While doing so they should usually also provide default implementations of their own APIs, for example, the `catalog` plugin exports `catalogApiRef`, and also -supplies a default `ApiFactory` of that API using the `CatalogClient`. There is -one restriction to plugin-provided API Factories: plugins may not supply -factories for core APIs, trying to do so will cause the app to refuse to start. +supplies a default [ApiFactory](../reference/core-plugin-api.apifactory.md) of +that API using the `CatalogClient`. There is one restriction to plugin-provided +API Factories: plugins may not supply factories for core APIs, trying to do so +will cause the app to refuse to start. -Plugins supply their APIs through the `apis` option of `createPlugin`, for -example: +Plugins supply their APIs through the `apis` option of +[createPlugin](../reference/core-plugin-api.createplugin.md), for example: ```ts export const techdocsPlugin = createPlugin({ @@ -144,7 +168,8 @@ Lastly, the app itself is the final point where APIs can be added, and what has the final say in what APIs will be loaded at runtime. The app may override the factories for any of the core or plugin APIs, with the exception of the config, app theme, and identity APIs. These are static APIs that are tied into the -`createApp` implementation, and therefore not possible to override. +[createApp](../reference/core-app-api.createapp.md) implementation, and +therefore not possible to override. Overriding APIs is useful for apps that want to switch out behavior to tailor it to their environment. In some cases plugins may also export multiple @@ -206,16 +231,19 @@ const app = createApp({ ``` Note that the above line will cause an error if `IgnoreErrorApi` does not fully -implement the `ErrorApi`, as it is checked by the type embedded in the -`errorApiRef` at compile time. +implement the [ErrorApi](../reference/core-plugin-api.errorapi.md), as it is +checked by the type embedded in the +[errorApiRef](../reference/core-plugin-api.errorapiref.md) at compile time. ## Defining custom Utility APIs Plugins are free to define their own Utility APIs. Simply define the TypeScript -interface for the API, and create an `ApiRef` using `createApiRef` exported from -`@backstage/core-plugin-api`. Also be sure to provide at least one -implementation of the API, and to declare a default factory for the API in -`createPlugin`. +interface for the API, and create an +[ApiRef](../reference/core-plugin-api.apiref.md) using +[createApiRef](../reference/core-plugin-api.createapiref.md) exported from +[@backstage/core-plugin-api](../reference/core-plugin-api.md). Also be sure to +provide at least one implementation of the API, and to declare a default factory +for the API in [createPlugin](../reference/core-plugin-api.createplugin.md). Custom Utility APIs can be either public or private, which is up to the plugin to choose. Private APIs do not expose an external API surface, and it's @@ -226,15 +254,18 @@ plugin to override the API in the app. It is however important to maintain backwards compatibility of public APIs, as you may otherwise break apps that are using your plugin. -To make an API public, simply export the `ApiRef` of the API, and any associated -types. To make an API private, just avoid exporting the `ApiRef`, but still be -sure to supply a default factory to `createPlugin`. +To make an API public, simply export the +[ApiRef](../reference/core-plugin-api.apiref.md) of the API, and any associated +types. To make an API private, just avoid exporting the +[ApiRef](../reference/core-plugin-api.apiref.md), but still be sure to supply a +default factory to [createPlugin](../reference/core-plugin-api.createplugin.md). Private APIs are useful for plugins that want to depend on other APIs outside of React components, but not have to expose an entire API surface to maintain. When using private APIs, it is fine to use the `typeof` of an implementing class as -the type parameter passed to `createApiRef`, while public APIs should always -define a separate TypeScript interface type. +the type parameter passed to +[createApiRef](../reference/core-plugin-api.createapiref.md), while public APIs +should always define a separate TypeScript interface type. Plugins may depend on APIs from other plugins, both in React components and as dependencies to API factories. Do however be sure to not cause circular @@ -242,13 +273,14 @@ dependencies between plugins. ## Architecture -The `ApiRef` instances mentioned above provide a point of indirection between -consumers and producers of Utility APIs. It allows for plugins and components to -depend on APIs in a type-safe way, without having a direct reference to a -concrete implementation of the APIs. The Apps are also given a lot of -flexibility in what implementations to provide. As long as they adhere to the -contract established by an `ApiRef`, they are free to choose any implementation -they want. +The [ApiRef](../reference/core-plugin-api.apiref.md) instances mentioned above +provide a point of indirection between consumers and producers of Utility APIs. +It allows for plugins and components to depend on APIs in a type-safe way, +without having a direct reference to a concrete implementation of the APIs. The +Apps are also given a lot of flexibility in what implementations to provide. As +long as they adhere to the contract established by an +[ApiRef](../reference/core-plugin-api.apiref.md), they are free to choose any +implementation they want. The figure below shows the relationship between different Apps, that provide @@ -271,14 +303,17 @@ directly tied to React. The indirection provided by Utility APIs also makes it straightforward to test components that depend on APIs, and to provide a standard common development environment for plugins. A proper test wrapper with mocked API implementations -is not yet ready, but it will be provided as a part of `@backstage/test-utils`. -It will provide mocked variants of APIs, with additional methods for asserting a -component's interaction with the API. +is not yet ready, but it will be provided as a part of +[@backstage/test-utils](../reference/test-utils.md). It will provide mocked +variants of APIs, with additional methods for asserting a component's +interaction with the API. The common development environment for plugins is included in -`@backstage/dev-utils`, where the exported `createDevApp` function creates an +[@backstage/dev-utils](../reference/dev-utils.md), where the exported +[createDevApp](../reference/dev-utils.createdevapp.md) function creates an application with implementations for all core APIs already present. Contrary to the method for wiring up Utility API implementations in an app created with -`createApp`, `createDevApp` uses automatic dependency injection. This is to make -it possible to replace any API implementation, and having that be reflected in -dependents of that API. +[createApp](../reference/core-app-api.createapp.md), +[createDevApp](../reference/dev-utils.createdevapp.md) uses automatic dependency +injection. This is to make it possible to replace any API implementation, and +having that be reflected in dependents of that API. diff --git a/docs/assets/features/catalog/life-of-an-entity_ingestion.svg b/docs/assets/features/catalog/life-of-an-entity_ingestion.svg new file mode 100644 index 0000000000..b250744fcd --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_ingestion.svg @@ -0,0 +1,3 @@ + + +
Unprocessed Entities
Unprocessed Entities
change events
change events
Entity Provider
Entity Provider
change events
change events
Entity Provider
Entity Provider
External
Source
External...
External
Source
External...
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_overview.svg b/docs/assets/features/catalog/life-of-an-entity_overview.svg new file mode 100644 index 0000000000..3fc35b4775 --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_overview.svg @@ -0,0 +1,3 @@ + + +
Unprocessed Entities
Unprocessed Entities
Processed Entities
Processed Entities
Relations
Relations
Edges
Edges
Final Entities
Final Entities
Search
Search
Errors
Errors
Processors
Processors
Stitcher
Stitcher
Entity Providers
Entity Providers
External
Sources
External...
Catalog
API
Catalog...
Processing
Processing
Stitching
Stitching
Ingestion
Ingestion
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_processing.svg b/docs/assets/features/catalog/life-of-an-entity_processing.svg new file mode 100644 index 0000000000..da3c4c7dba --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_processing.svg @@ -0,0 +1,3 @@ + + +
Processor One
Processor One
Processor Two
Processor Two
Unprocessed Entity
Unprocessed Enti...
emit
e...
preprocess
preprocess

preprocess

preprocess
(intermediate)
(intermediate)
validate
validate
(intermediate)
(intermediate)

postprocess

postprocess
(intermediate)
(intermediate)
Processed
Entity
Processed...


relations,
errors,
other entities
relations,...
emit
e...


relations,
errors,
other entities
relations,...
emit
e...


relations,
errors,
other entities
relations,...
emit
e...


relations,
errors,
other entities
relations,...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_stitching.svg b/docs/assets/features/catalog/life-of-an-entity_stitching.svg new file mode 100644 index 0000000000..3b232a361c --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_stitching.svg @@ -0,0 +1,3 @@ + + +
Processed
Entity
Processed...
Processed
Entity
Processed...
Processed
Entity
Processed...
Final
Entity
Final...
Errors
Errors
Errors
Errors
Errors
Errors
Stitcher
Stitcher
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Search
Search
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 56586cbfc6..472a6a0abc 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -244,9 +244,7 @@ export interface OAuthProviderHandlers { req: express.Request, options: Record, ): Promise; - handler( - req: express.Request, - ): Promise<{ + handler(req: express.Request): Promise<{ response: AuthResponse; refreshToken?: string; }>; diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 4b65c8c3c2..81b2e98f2c 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -10,11 +10,16 @@ that can authenticate users using GitHub or GitHub Enterprise OAuth. ## Create an OAuth App on GitHub -To add GitHub authentication, you must create an OAuth App from the GitHub +To add GitHub authentication, you must create either a GitHub App, or an OAuth +App from the GitHub [developer settings](https://github.com/settings/developers). The `Homepage URL` should point to Backstage's frontend, while the `Authorization callback URL` will point to the auth backend. +Note that if you're using a GitHub App, the allowed scopes are configured as +part of that app. This means you need to verify what scopes the plugins you use +require, so be sure to check the plugin READMEs for that information. + Settings for local development: - Application name: Backstage (or your custom app name) @@ -51,3 +56,11 @@ The GitHub provider is a structure with three configuration keys: To add the provider to the frontend, add the `githubAuthApi` reference and `SignInPage` component as shown in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). + +## Difference between GitHub Apps and GitHub OAuth Apps + +GitHub Apps handle OAuth scope at the app installation level, meaning that the +`scope` parameter for the call to `getAccessToken` in the frontend has no +effect. When calling `getAccessToken` in open source plugins, one should still +include the appropriate scope, but also document in the plugin README what +scopes are required for GitHub Apps. diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 814ff63729..2c36ddcd7f 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -111,6 +111,48 @@ export default async function createPlugin({ ... ``` +## Resolving membership through the catalog + +If you want to provide additional claims through Sign-In resolvers but still +have the software catalog handle group (and transitive group) membership, you +can do this using the `CatalogIdentityClient` provided as context to Sign-In +resolvers: + +```ts +export default async function createPlugin({ + ... +}: PluginEnvironment): Promise { + return await createRouter({ + ... + providerFactories: { + google: createGoogleProvider({ + signIn: { + resolver: async ({ profile: { email } }, ctx) => { + const [id] = email?.split('@') ?? ''; + // Fetch from an external system that returns entity claims like: + // ['user:default/breanna.davison', ...] + const ent = await externalSystemClient.getUsernames(email); + + // Resolve group membership from the Backstage catalog + const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [id].concat(ent), + logger: ctx.logger, + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: fullEnt }, + }); + return { id, token }; + }, + }, + }), + ... +``` + +The `resolveCatalogMembership` method will retrieve the referenced entities from +the catalog, if possible, and check for +[memberOf](../features/software-catalog/well-known-relations.md#memberof-and-hasmember) +relations to add additional entity claims. + ## AuthHandler Similar to a custom sign-in resolver, you can also write a custom auth handler diff --git a/docs/auth/index.md b/docs/auth/index.md index 76cd79b0fd..0c03e33900 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -60,9 +60,8 @@ small update to show this provider as a login option. The `SignInPage` component handles this, and takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. -These reference the [ApiRef](../reference/utility-apis/README.md) exported by -the provider. Again, an example using GitHub that can be adapted to any of the -built-in providers: +These reference the `ApiRef` exported by the provider. Again, an example using +GitHub that can be adapted to any of the built-in providers: ```diff # packages/app/src/App.tsx diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index b64f9b1902..52f463bab4 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -44,7 +44,7 @@ The Microsoft provider is a structure with three configuration keys: - `clientId`: Application (client) ID, found on App Registration > Overview - `clientSecret`: Secret, found on App Registration > Certificates & secrets -- `tenentId`: Directory (tenant) ID, found on App Registration > Overview +- `tenantId`: Directory (tenant) ID, found on App Registration > Overview ## Adding the provider to the Backstage frontend diff --git a/docs/auth/using-auth.md b/docs/auth/using-auth.md index e4f87a12f9..067c43e8a0 100644 --- a/docs/auth/using-auth.md +++ b/docs/auth/using-auth.md @@ -28,7 +28,8 @@ OAuth helps in that regard. The method with which frontend plugins request access to third party services is through [Utility APIs](../api/utility-apis.md) for each service provider. For a full list of providers, see the -[Utility API References](../reference/utility-apis/README.md). +[@backstage/core-plugin-api](../reference/core-plugin-api.md#variables) +reference. ### Identity - WIP diff --git a/docs/conf/reading.md b/docs/conf/reading.md index 6ac10a8c8b..fd1ce4c7a6 100644 --- a/docs/conf/reading.md +++ b/docs/conf/reading.md @@ -7,7 +7,7 @@ description: Documentation on Reading Backstage Configuration ## Config API There's a common configuration API for by both frontend and backend plugins. An -API reference can be found [here](../reference/utility-apis/Config.md). +API reference can be found [here](../reference/config.config.md). The configuration API is tailored towards failing fast in case of missing or bad config. That's because configuration errors can always be considered programming @@ -110,9 +110,18 @@ example `getString`. These will throw an error if there is no value available. ## Accessing ConfigApi in Frontend Plugins -The [ConfigApi](../reference/utility-apis/Config.md) in the frontend is a +The [ConfigApi](../reference/core-plugin-api.configapi.md) in the frontend is a [UtilityApi](../api/utility-apis.md). It's accessible as usual via the -`configApiRef` exported from `@backstage/core-plugin-api`. +`configApiRef` exported from `@backstage/core-plugin-api`: + +``` +import { useApi, configApiRef } from '@backstage/core-plugin-api'; +... +const MyReactComponent = (...) => { + const config = useApi(configApiRef); + ... +} +``` Depending on the config api in another API is slightly different though, as the `ConfigApi` implementation is supplied via the App itself and not instantiated diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index e542b02ebf..599c01a0ce 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -113,6 +113,9 @@ browser at `http://localhost:7000` ## Multi-stage Build +> NOTE: The `.dockerignore` is different in this setup, read on for more +> details. + This section describes how to set up a multi-stage Docker build that builds the entire project within Docker. This is typically slower than a host build, but is sometimes desired because Docker in Docker is not available in the build @@ -137,6 +140,7 @@ WORKDIR /app COPY package.json yarn.lock ./ COPY packages packages +# Comment this out if you don't have any internal plugins COPY plugins plugins RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ @@ -182,8 +186,9 @@ end up being properly installed. To speed up the build when not running in a fresh clone of the repo you should set up a `.dockerignore`. This one is different than the host build one, because -we want to have access to the source code of all packages for the build, but can -ignore any existing build output or dependencies: +we want to have access to the source code of all packages for the build. We can +however ignore any existing build output or dependencies on the host. For our +new `.dockerignore`, replace the contents of your existing one with this: ```text node_modules diff --git a/docs/features/search/architecture.md b/docs/features/search/architecture.md index 0a1ebea4b0..4fac0e7cff 100644 --- a/docs/features/search/architecture.md +++ b/docs/features/search/architecture.md @@ -4,8 +4,6 @@ title: Search Architecture description: Documentation on Search Architecture --- -# Search Architecture - > _This architecture has not been fully implemented yet. Find our milestones to > follow our progress and help contribute on the > [Search Roadmap](./README.md#project-roadmap)._ diff --git a/docs/features/search/concepts.md b/docs/features/search/concepts.md index 5da7c9c58f..8cb17b12e6 100644 --- a/docs/features/search/concepts.md +++ b/docs/features/search/concepts.md @@ -4,8 +4,6 @@ title: Search Concepts description: Documentation on Backstage Search Concepts --- -# Search Concepts - Backstage Search lets you find the right information you are looking for in the Backstage ecosystem. diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 2bd26faf8b..aa40c1e539 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -4,8 +4,6 @@ title: Getting Started with Search description: How to set up and install Backstage Search --- -# Getting Started - Search functions as a plugin to Backstage, so you will need to use Backstage to use Search. @@ -252,13 +250,9 @@ an example: Backstage Search isn't a search engine itself, rather, it provides an interface between your Backstage instance and a [Search Engine](./concepts.md#search-engines) of your choice. Currently, we only -support one, an in-memory search Engine called Lunr. It can be instantiated like -this: - -```typescript -const searchEngine = new LunrSearchEngine({ logger }); -const indexBuilder = new IndexBuilder({ logger, searchEngine }); -``` +support two engines, an in-memory search Engine called Lunr and ElasticSearch. +See [Search Engines](./search-engines.md) documentation for more information how +to configure these in your Backstage instance. Backstage Search can be used to power search of anything! Plugins like the Catalog offer default [collators](./concepts.md#collators) (e.g. diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md new file mode 100644 index 0000000000..a0302ce279 --- /dev/null +++ b/docs/features/search/search-engines.md @@ -0,0 +1,158 @@ +--- +id: search-engines +title: Search Engines +description: Choosing and configuring your search engine for Backstage +--- + +Backstage supports 2 search engines by default, an in-memory engine called Lunr +and ElasticSearch. You can configure your own search engines by implementing the +provided interface as mentioned in the +[search backend documentation.](./getting-started.md#Backend) + +Provided search engine implementations have their own way of constructing +queries, which may be something you want to modify. Alterations to the querying +logic of a search engine can be made by providing your own implementation of a +QueryTranslator interface. This modification can be done without touching +provided search engines by using the exposed setter to set the modified query +translator into the instance. + +```typescript +const searchEngine = new LunrSearchEngine({ logger }); +searchEngine.setTranslator(new MyNewAndBetterQueryTranslator()); +``` + +## Lunr + +Lunr search engine is enabled by default for your backstage instance if you have +not done additional changes to the scaffolded app. + +Lunr can be instantiated like this: + +```typescript +// app/backend/src/plugins/search.ts +const searchEngine = new LunrSearchEngine({ logger }); +const indexBuilder = new IndexBuilder({ logger, searchEngine }); +``` + +## Postgres + +The Postgres based search engine only requires that postgres being configured as +the database engine for Backstage. Therefore it targets setups that want to +avoid maintaining another external service like elastic search. The search +provides decent results and performs well with ten thousands of indexed +documents. The connection to postgres is established via the database manager +also used by other plugins. + +> **Important**: The search plugin requires at least Postgres 12! + +To use the `PgSearchEngine`, make sure that you have a Postgres database +configured and make the following changes to your backend: + +1. Add a dependency on `@backstage/plugin-search-backend-module-pg` to your + backend's `package.json`. +2. Initialize the search engine. It is recommended to initialize it with a + fallback to the lunr search engine if you are running Backstage for + development locally with SQLite: + +```typescript +// In packages/backend/src/plugins/search.ts + +// Initialize a connection to a search engine. +const searchEngine = (await PgSearchEngine.supported(database)) + ? await PgSearchEngine.from({ database }) + : new LunrSearchEngine({ logger }); +``` + +## ElasticSearch + +Backstage supports ElasticSearch search engine connections, indexing and +querying out of the box. Available configuration options enable usage of either +AWS or Elastic.co hosted solutions, or a custom self-hosted solution. + +Similarly to Lunr above, ElasticSearch can be set up like this: + +```typescript +// app/backend/src/plugins/search.ts +const searchEngine = await ElasticSearchSearchEngine.initialize({ + logger, + config, +}); +const indexBuilder = new IndexBuilder({ logger, searchEngine }); +``` + +For the engine to be available, your backend package needs a dependency into +package `@backstage/plugin-search-backend-module-elasticsearch`. + +ElasticSearch needs some additional configuration before it is ready to use +within your instance. The configuration options are documented in the +[configuration schema definition file.](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-elasticsearch/config.d.ts) + +The underlying functionality is using official ElasticSearch client version 7.x, +meaning that ElasticSearch version 7 is the only one confirmed to be supported. + +## Example configurations + +### AWS + +Using AWS hosted ElasticSearch the only configuration option needed is the URL +to the ElasticSearch service. The implementation assumes that environment +variables for AWS access key id and secret access key are defined in accordance +to the +[default AWS credential chain.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html). + +```yaml +search: + elasticsearch: + provider: aws + node: https://my-backstage-search-asdfqwerty.eu-west-1.es.amazonaws.com +``` + +### Elastic.co + +Elastic Cloud hosted ElasticSearch uses a Cloud ID to determine the instance of +hosted ElasticSearch to connect to. Additionally, username and password needs to +be provided either directly or using environment variables like defined in +[Backstage documentation.](https://backstage.io/docs/conf/writing#includes-and-dynamic-data) + +```yaml +search: + elasticsearch: + provider: elastic + cloudId: backstage-elastic:asdfqwertyasdfqwertyasdfqwertyasdfqwerty== + auth: + username: elastic + password: changeme +``` + +### Others + +Other ElasticSearch instances can be connected to by using standard +ElasticSearch authentication methods and exposed URL, provided that the cluster +supports that. The configuration options needed are the URL to the node and +authentication information. Authentication can be handled by either providing +username/password or an API key. For more information how to create an API key, +see +[Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html). + +#### Configuration examples + +##### With username and password + +```yaml +search: + elasticsearch: + node: http://localhost:9200 + auth: + username: elastic + password: changeme +``` + +##### With API key + +```yaml +search: + elasticsearch: + node: http://localhost:9200 + auth: + apiKey: base64EncodedKey +``` diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index e0228bf7d0..8f855776a0 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -27,27 +27,32 @@ default catalog page and create a component in a ```tsx // imports, etc omitted for brevity. for full source see: // https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx -export const CustomCatalogPage = () => { +export const CustomCatalogPage = ({ + columns, + actions, + initiallySelectedFilter = 'owned', +}: CatalogPageProps) => { return ( - - - - - All your software catalog entities - -

- -
-
- -
-
-
- + + + + + + + + ); }; ``` @@ -137,19 +142,27 @@ export const EntitySecurityTierPicker = () => { Now we can add the component to `CustomCatalogPage`: ```diff -export const CustomCatalogPage = () => { +export const CustomCatalogPage = ({ + columns, + actions, + initiallySelectedFilter = 'owned', +}: CatalogPageProps) => { return ( ... - -
+ + +
- -
+ + + + + + ... }; ``` @@ -173,7 +186,7 @@ new `CustomCatalogIndexPage`. # packages/app/src/App.tsx const routes = ( - + - } /> + } /> ``` diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index fb17f4a434..7a9aa099e9 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -245,6 +245,22 @@ the entity belongs to the `"default"` namespace. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. +### `title` [optional] + +A display name of the entity, to be presented in user interfaces instead of the +`name` property above, when available. + +This field is sometimes useful when the `name` is cumbersome or ends up being +perceived as overly technical. The title generally does not have as stringent +format requirements on it, so it may contain special characters and be more +explanatory. Do keep it very short though, and avoid situations where a title +can be confused with the name of another entity, or where two entities share a +title. + +Note that this is only for display purposes, and may be ignored by some parts of +the code. [Entity references](references.md) still always make use of the `name` +property for example, not the title. + ### `description` [optional] A human readable description of the entity, to be shown in Backstage. Should be @@ -531,8 +547,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the component, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +component, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a component is the singular entity (commonly a team) that bears ultimate responsibility for the component, and has the authority and @@ -550,8 +566,8 @@ component, but there will always be one ultimate owner. ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the component -belongs to, e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +component belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -559,8 +575,8 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional. ### `spec.subcomponentOf` [optional] -An [entity reference](#string-references) to another component of which the -component is a part, e.g. `spotify-ios-app`. This field is optional. +An [entity reference](references.md#string-references) to another component of +which the component is a part, e.g. `spotify-ios-app`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -568,8 +584,8 @@ component is a part, e.g. `spotify-ios-app`. This field is optional. ### `spec.providesApis` [optional] -An array of [entity references](#string-references) to the APIs that are -provided by the component, e.g. `artist-api`. This field is optional. +An array of [entity references](references.md#string-references) to the APIs +that are provided by the component, e.g. `artist-api`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -577,8 +593,8 @@ provided by the component, e.g. `artist-api`. This field is optional. ### `spec.consumesApis` [optional] -An array of [entity references](#string-references) to the APIs that are -consumed by the component, e.g. `artist-api`. This field is optional. +An array of [entity references](references.md#string-references) to the APIs +that are consumed by the component, e.g. `artist-api`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -586,9 +602,9 @@ consumed by the component, e.g. `artist-api`. This field is optional. ### `spec.dependsOn` [optional] -An array of [entity references](#string-references) to the components and -resources that the component depends on, e.g. `artists-db`. This field is -optional. +An array of [entity references](references.md#string-references) to the +components and resources that the component depends on, e.g. `artists-db`. This +field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | @@ -690,12 +706,6 @@ shape, this kind has the following structure. Exactly equal to `backstage.io/v1beta2` and `Template`, respectively. -### `metadata.title` [required] - -The nice display name for the template as a string, e.g. `React SSR Template`. -This field is required as is used to reference the template to the user instead -of the `metadata.name` field. - ### `metadata.tags` [optional] A list of strings that can be associated with the template, e.g. @@ -722,8 +732,8 @@ You can find out more about the `steps` key ### `spec.owner` [optional] -An [entity reference](#string-references) to the owner of the template, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +template, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a Template is the singular entity (commonly a team) that bears ultimate responsibility for the Template, and has the authority and @@ -832,8 +842,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the component, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +component, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of an API is the singular entity (commonly a team) that bears ultimate responsibility for the API, and has the authority and capability @@ -851,8 +861,8 @@ one ultimate owner. ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the API belongs to, -e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +API belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1059,8 +1069,8 @@ Exactly equal to `backstage.io/v1alpha1` and `Resource`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the resource, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +resource, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a resource is the singular entity (commonly a team) that bears ultimate responsibility for the resource, and has the authority and @@ -1091,8 +1101,8 @@ Some common values for this field could be: ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the resource -belongs to, e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +resource belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1100,9 +1110,9 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional. ### `spec.dependsOn` [optional] -An array of [entity references](#string-references) to the components and -resources that the resource depends on, e.g. `artist-lookup`. This field is -optional. +An array of [entity references](references.md#string-references) to the +components and resources that the resource depends on, e.g. `artist-lookup`. +This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | @@ -1146,8 +1156,8 @@ Exactly equal to `backstage.io/v1alpha1` and `System`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the system, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +system, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a system is the singular entity (commonly a team) that bears ultimate responsibility for the system, and has the authority and @@ -1165,8 +1175,8 @@ but there will always be one ultimate owner. ### `spec.domain` [optional] -An [entity reference](#string-references) to the domain that the system belongs -to, e.g. `artists`. This field is optional. +An [entity reference](references.md#string-references) to the domain that the +system belongs to, e.g. `artists`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1205,8 +1215,8 @@ Exactly equal to `backstage.io/v1alpha1` and `Domain`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the domain, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +domain, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a domain is the singular entity (commonly a team) that bears ultimate responsibility for the domain, and has the authority and diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index aa5aed0540..ed106bb5dd 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -393,3 +393,39 @@ of interest to you, you might consider contacting the maintainers on Discord or my making a GitHub issue describing your use case. [This issue](https://github.com/backstage/backstage/issues/2292) also contains more context. + +## Referencing different environments with the model + +Example intent: + +> "I have multiple versions of my API deployed in different environments so I +> want to have `mytool-dev` and `mytool-prod` as different entities." + +While it's possible to have different versions of the same thing represented as +separate entities, it's something we generally recommend against. We believe +that a developer should be able to just find for example one `Component` +representing a service, and to be able to see the different code versions that +are deployed throughout your stack within its view. This reasoning works +similarly for other kinds as well, such as `API`. + +That being said - sometimes the differences between versions are so large, that +they represent what is for all intents and purposes an entirely new entity as +seen from the consumer's point of view. This can happen for example for +different _significant_ major versions of an API, and in particular if the two +major versions coexist in the ecosystem for some time. In those cases, it can be +motivated to have one `my-api-v2` and one `my-api-v3` named entity. This matches +the end user's expectations when searching for the API, and matches the desire +to maybe have separate documentation for the two and similar. But use this +sparingly - only do it if the extra modelling burden is outweighed by any +potential better clarity for users. + +When writing your custom plugins, we encourage designing them such that they can +show all the different variations through environments etc under one canonical +reference to your software in the catalog. For example for a continuous +deployment plugin, a user is likely to be greatly helped by being able to see +the entity's versions deployed in all different environments next to each other +in one view. That is also where they might be offered the ability to promote +from one environment to the other, do rollbacks, see their relative performance +metrics, and similar. This coherency and collection of tooling in one place is +where something like Backstage can offer the most value and effectiveness of +use. Splitting your entities apart into small islands makes this harder. diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index a502c2aee5..f5296f38e1 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -8,12 +8,11 @@ description: The Backstage Software Catalog ## What is a Software Catalog? -The Backstage Software Catalog — actually, a software catalog, since it includes -more than just services — is a centralized system that keeps track of ownership -and metadata for all the software in your ecosystem (services, websites, -libraries, data pipelines, etc). The catalog is built around the concept of -[metadata YAML files](descriptor-format.md) stored together with the code, which -are then harvested and visualized in Backstage. +The Backstage Software Catalog is a centralized system that keeps track of +ownership and metadata for all the software in your ecosystem (services, +websites, libraries, data pipelines, etc). The catalog is built around the +concept of [metadata YAML files](descriptor-format.md) stored together with the +code, which are then harvested and visualized in Backstage. ![software-catalog](https://backstage.io/blog/assets/6/header.png) diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md new file mode 100644 index 0000000000..9e8a3f0563 --- /dev/null +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -0,0 +1,246 @@ +--- +id: life-of-an-entity +title: The Life of an Entity +sidebar_label: The Life of an Entity +# prettier-ignore +description: The life cycle of entities, from being introduced into the catalog, through processing, to being removed again +--- + +This document gives a high level overview of the catalog backend, and the +technical processes involved in making entities flow through it. It is mainly +aimed at developers who want to understand the internals while installing or +extending the catalog. However, it can be informative for other personas too. + +## Key Concepts + +The catalog forms a hub of sorts, where entities are ingested from various +authoritative sources and held in a database, subject to automated processing, +and then presented through an API for quick and easy access by Backstage and +others. The most common source is [YAML files](descriptor-format.md) on a +standard format, living in version control systems near the source code of +systems that they describe. Those files are registered with the catalog and +maintained by the respective owners. The catalog makes sure to keep itself up to +date with changes to those files. + +The main extension points where developers can customize the catalog are: + +- _Entity providers_, that feed initial raw entity data into the catalog, +- _Policies_, that establish baseline rules about the shape of entities, +- _Processors_, that validate, analyze, and mutate the raw entity data into its + final form. + +The high level processes involved are: + +- _Ingestion_, where entity providers fetch raw entity data from external + sources and seed it into the database, +- _Processing_, where the policies and processors continually treat the ingested + data and may emit both other raw entities (that are also subject to + processing), errors, relations to other entities, etc., +- _Stitching_, where all of the data emitted by various processors are assembled + together into the final output entity. + +An entity is not visible to the outside world (through the catalog API), until +it has passed through the last process and landed among the final entities. + +![General overview](../../assets/features/catalog/life-of-an-entity_overview.svg) + +The details of these processes are described below. + +## Ingestion + +Each catalog deployment has a number of entity providers installed. They are +responsible for fetching data from external authoritative sources in any way +that they see fit, to translate those into entity objects, and to notify the +database when those entities are added or removed. These are the _unprocessed +entities_ that will be subject to later processing (see below), and they form +the very basis of existence for entities. If there were no entity providers, no +entities would ever enter the system. + +The database always keeps track of the set of entities that belong to each +provider; no two providers can try to output the same entity. And when a +provider signals the removal of an entity, then that leads to an _eager +deletion_: the entity and all auxiliary data that it has led to in the database +is immediately purged. + +![Ingestion overview](../../assets/features/catalog/life-of-an-entity_ingestion.svg) + +There are two providers installed by default: the one that deals with user +registered locations (e.g. URLs to YAML files), and the one that deals with +static locations in the app-config. You can add more third party providers by +passing them to the catalog builder in your backend initialization code, and you +can easily write your own. + +An entity provider is a class that implements the `EntityProvider` interface. It +has three main parts: + +- The identity: Each provider instance has a unique, stable identifier that the + database can use to keep track of the originator of each unprocessed entity. +- The connection: During backend startup, each provider is attached to the + catalog runtime. +- The stream of events: During its lifetime, the provider can issue change + events to the runtime at any point in time, to modify its set of unprocessed + entities. + +It is entirely up to the provider to choose how and when it produces these +change events. For example, the app-config provider only fires off an update at +startup and then lies dormant. The location database provider does an initial +update at startup, and then small delta updates every time a location database +change is detected. The LDAP provider is driven externally by a timer loop that +occasionally triggers a full update. Some future provider may be entirely event +driven, feeding off an event bus or web hook. There is no magic coordination +among providers; if they need to arrange synchronization or locking among +themselves for example to avoid duplicate work across multiple catalog service +machines, they need to handle that out-of-band. + +The entities that are emitted get some coarse validation applied to them, to +ensure that they at least adhere to the most basic schema rules about how an +entity should be shaped. For example, they need to have a `kind`, a +`metadata.name`, and optionally a `metadata.namespace`, among others. Apart from +that, the ingestion stage considers its work done, and stores the unprocessed +entities to be picked up at a later time by the processing system. This means +that the more precise validation rules that you put in place on entities are +_not_ yet applied at this stage. + +## Processing + +Every unprocessed entity comes with a timestamp, which tells at what time that +the processing loop should next try to process it. When the entity first +appears, this timestamp is set to "now" - asking for it to be picked up as soon +as possible. + +Each catalog deployment has a number of processors installed. They are +responsible for receiving unprocessed entities that the catalog decided are due +for processing, and then running that data through a number of processing +stages, mutating the entity and emitting auxiliary data about it. When all of +that is done, the catalog takes all of that information and stores it as the +processed entity, and errors and relations to other entities separately. Then, +the catalog checks to see what entities are touched by that output, and triggers +the final assembly of those (see Stitching below). + +![Processing overview](../../assets/features/catalog/life-of-an-entity_processing.svg) + +Entities are always processed one by one, but all of your catalog service hosts +collaborate in doing so to distribute the load. Note how each processor can +contribute to one or more of the fixed steps in the processing pipeline. First +all of the processors' contributions to one step are run in the order that the +processors were registered, then all of their contributions to the next step in +the same order, and so on. + +Each step has the opportunity to optionally modify the entity, and to optionally +emit other information. For example, the processor might look at information in +the `spec` field of the entity, and emit relations that correspond to those +declarations. If the processor emits an entity, then that entity gets stored +verbatim with a timestamp saying that it, too, should be processed as soon as +possible. If errors are emitted, then that signals that something is wrong with +the entity and that it should not replace whatever previously error-free version +we had among the final entities. If relations are emitted, then they are put in +a dedicated relations table to be picked up by the stitching process below. + +> Optional low level detail note: When entities are emitted, the catalog keeps +> track of the edges between the emitting entity and the ones emitted. This +> happens behind the scenes, hidden from the outside, and is used to form a +> graph. This is _not_ the same thing as relations! The purpose of these edges, +> is to be able to detect when an entity becomes orphaned (see below), and to be +> able to perform eager deletions throughout the graph when a root is explicitly +> unregistered and nothing else is keeping lower nodes alive. We will talk more +> about orphaning and deletions later on in this article. + +When the final step has completed, and no errors were encountered, the processed +entity and all of the relations are finally persisted in the database. Then the +catalog considers this entity, and all of the entities it had relations to, +subject for stitching. + +It is worth noting here that the processing does not lead to deletion or +unregistration of entities; it can only call new entities into existence or +update entities that it has previously called into existence. More about that +later. + +## Stitching + +Stitching finalizes the entity, by gathering all of the output from the previous +steps and merging them into the final object which is what is visible from the +catalog API. As the final entity itself gets updated, the stitcher makes sure +that the search table gets refreshed accordingly as well. + +> Note: The search table mentioned here is not related to the core Search +> feature of Backstage. It's rather the table that backs the ability to filter +> catalog API query results. + +![Stitching overview](../../assets/features/catalog/life-of-an-entity_stitching.svg) + +The diagram shows how the stitcher reads from several sources: + +- The processed entity, as returned from the processing step +- The errors, if any, that were emitted by the processing step +- All relations that were emitted by the processing step, as well as any + relations emitted by _other_ entity processing steps that happen to point at + the current entity + +The last part is noteworthy: This is how the stitcher is able to collect all of +the relation edges, both incoming and outgoing, no matter who produced them. + +The stitching is currently a fixed process, that cannot be modified or extended. +This means that any modifications you want to make on the final result, has to +happen during ingestion or processing. + +## Errors + +> TODO: Describe how errors are exposed through entities + +## Orphaning + +As mentioned earlier, entities internally form a graph. The edges go from +processed parent entities, to child entities emitted while processing the +parent. + +The processing loop runs continuously, so these edges are reconsidered over +time. If processing a parent entity no longer emits a given child entity, then +that former edge is severed. If that child has no other edges pointing at it +either, it becomes _orphaned_. The end result is as follows: + +- The stitching process injects a `backstage.io/orphan: 'true'` annotation on + the child entity. +- The child entity is _not_ removed from the catalog, but stays around until + explicitly deleted via the catalog API, or "reclaimed" by the original parent + or another parent starting to reference it. +- The catalog page in Backstage for the child entity detects the new annotation + and informs users about the orphan status. + +Orphaning can occur in several different scenarios. One common cause is that the +end user edited a corresponding catalog catalog-info YAML file removing the +entity's entry. In the case of a `Location` parent entity, orphaning can happen +if removing the target line pointing to the file containing the child entity. +Another common cause is large batch processors such as the ones that crawl +through remote systems looking for entities, no longer finding something that it +used to find before. Maybe the data was moved, or deleted, in the remote system. +So for example when a person leaves the company an LDAP org discovery processor +might leave an orphaned `User` entity behind. Note that this only applies to +processors - ingestion that happens using entity providers work differently, +described below. + +> Note that removing a file, or accidentally corrupting a file so that it cannot +> be read successfully, does _not_ lead to orphaning. Hard errors, including the +> inability to find or read a distinct remote, are marked as such on the entity +> to inform the owner that something is wrong. But processing and other +> behaviors continue as usual. + +The reason that the orphaning mechanism exists instead of having an eager +deletion triggered, is safety. Scenarios like these can happen purely by +accident, due to the asynchronous nature of the system and the fallible nature +of humans. In particular when external systems start consuming and relying on +the catalog, there could be substantial consequences to suddenly dropping +entities without explicit owner consent. The catalog therefore takes the stance +that entities that often were added by direct user action should also be deleted +only by direct user action. + +It is possible to use the catalog API to build automated "reaper" systems that +finally delete entities that are orphaned. This is however not something that's +provided out of the box. + +## Implicit Deletion + +> TODO: Describe the process of entity providers eagerly deleting entities + +## Explicit Deletion + +> TODO: Describe direct deletion via the catalog API diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 67b92bd941..c3af6caf2d 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -57,18 +57,48 @@ if the original location delegates to another location. A common case is, that a location is registered as `bootstrap:bootstrap` which means that it is part of the `app-config.yaml` of a Backstage installation. +### backstage.io/orphan + +This annotation is either absent, or present with the exact _string_ value +`"true"`. It should never be added manually. Instead, the catalog itself injects +the annotation as part of its processing loops, on entities that are found to +have no registered locations or config locations that keep them "active" / +"alive". + +For example, suppose that the user first registers a location URL pointing to a +`Location` kind entity, which in turn refers to two `Component` kind entities in +two other files nearby. The end result is that the catalog contains those three +entities. Now suppose that the user edits the original `Location` entity to only +refer to the first of the `Component` kind entities. This will intentionally +_not_ lead to the other `Component` entity to be removed from the catalog (for +safety reasons). Instead, it gains this orphan marker annotation, to make it +clear that user action is required to completely remove it, if desired. + +```yaml +# Example: +metadata: + annotations: + backstage.io/orphan: 'true' +``` + ### backstage.io/techdocs-ref ```yaml # Example: metadata: annotations: - backstage.io/techdocs-ref: url:https://github.com/backstage/backstage/tree/master + backstage.io/techdocs-ref: dir:. ``` -The value of this annotation is a location reference string (see above). If this -annotation is specified, it is expected to point to a repository that the -TechDocs system can read and generate docs from. +The value of this annotation informs _where_ TechDocs source content is stored +so that it can be read and docs can be generated from it. Most commonly, it's +written as a path, relative to the location of the `catalog-info.yaml` itself, +where the associated `mkdocs.yml` file can be found. + +In unusual situations where the documentation for a catalog entity does not live +alongside the entity's source code, the value of this annotation can point to an +absolute URL, matching the location reference string format outlined above, for +example: `url:https://github.com/backstage/backstage/tree/master` ### backstage.io/view-url, backstage.io/edit-url @@ -101,18 +131,22 @@ repository itself. If the URL points to a folder, it is important that it is suffixed with a `'/'` in order for relative path resolution to work consistently. -### jenkins.io/github-folder +### jenkins.io/job-full-name ```yaml # Example: metadata: annotations: - jenkins.io/github-folder: folder-name/job-name + jenkins.io/job-full-name: folder-name/job-name ``` The value of this annotation is the path to a job on Jenkins, that builds this entity. +The value can be the format of just `[folder-path]` or +`[instanceName]:[folder-path]`, if multiple instances are configured in +`app-config.yaml` + Specifying this annotation may enable Jenkins related features in Backstage for that entity. @@ -304,6 +338,10 @@ This annotation allowed to load the API definition from another location. Use [substitution](./descriptor-format.md#substitutions-in-the-descriptor-format) instead. +### jenkins.io/github-folder + +Use the `jenkins.io/job-full-name` instead. + ## Links - [Descriptor Format: annotations](descriptor-format.md#annotations-optional) diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index cdd8166889..737f77f67b 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -33,9 +33,12 @@ scaffolder: ### Disabling Docker in Docker situation (Optional) -Software Templates use -[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as a templating -library. By default it will use the +Software templates use the `fetch:template` action by default, which requires no +external dependencies and offers a +[Cookiecutter-compatible mode](https://backstage.io/docs/features/software-templates/builtin-actions#using-cookiecuttercompat-mode). +There is also a `fetch:cookiecutter` action, which uses +[Cookiecutter](https://github.com/cookiecutter/cookiecutter) directly for +templating. By default, the `fetch:cookiecutter` action will use the [scaffolder-backend/Cookiecutter](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile) docker image. diff --git a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md index 25ff2a2f3a..28001cce0b 100644 --- a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md +++ b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md @@ -325,7 +325,7 @@ spec: output: links: - url: '{{steps.publish.output.remoteUrl}}' - text: 'Go to Repo' + title: 'Go to Repo' ``` ## Questions? diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 53a90e2954..a5773a6aff 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -131,6 +131,9 @@ want to have those as well as your new one, you'll need to do the following: ```ts import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; +import { ScmIntegrations } from '@backstage/integration'; + +const integrations = ScmIntegrations.fromConfig(config); const builtInActions = createBuiltinActions({ containerRunner, diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 7b475f2fa1..02bb698f64 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -227,6 +227,33 @@ spec: inputType: tel ``` +#### Hide or mask sensitive data on Review step + +Sometimes, specially in custom fields, you collect some data on Create form that +must not be shown to the user on Review step. To hide or mask this data, you can +use `ui:widget: password` or set some properties of `ui:backstage`: + +```yaml +- title: Hide or mask values + properties: + password: + title: Password + type: string + ui:widget: password # will print '******' as value for property 'password' on Review Step + masked: + title: Masked + type: string + ui:backstage: + review: + mask: '' # will print '' as value for property 'Masked' on Review Step + hidden: + title: Hidden + type: string + ui:backstage: + review: + show: false # wont print any info about 'hidden' property on Review Step +``` + #### The Repository Picker So in order to make working with repository providers easier, we've built a diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 6732b3ab18..b798742463 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -29,7 +29,7 @@ about TechDocs and the philosophy in its - Explore and take advantage of the large ecosystem of [MkDocs plugins](https://www.mkdocs.org/user-guide/plugins/) to create a rich reading experience. -- Search for and find docs (coming soon). +- Search for and find docs. - Highlight text and raise an Issue to create feedback loop to drive quality documentation (future). - Contribute to and deploy from a marketplace of TechDocs widgets (future). @@ -54,23 +54,12 @@ providers are used. | Google Cloud Storage (GCS) | Yes ✅ | | Amazon Web Services (AWS) S3 | Yes ✅ | | Azure Blob Storage | Yes ✅ | -| OpenStack Swift | Yes ✅ | +| OpenStack Swift | Community ✅ | [Reach out to us](#feedback) if you want to request more platforms. ## Project roadmap -### **Ongoing work 🚧** - -**Beta release** - -[Milestone](https://github.com/backstage/backstage/milestone/29) - -- It should be possible and easy to use TechDocs in most environments across - organizations. -- Minimal bugs, better error handling and scalable backend and frontend. -- Documentation Search -- TechDocs Homepage with basic features - ### **Future work 🔮** **General Availability (GA) release** - diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index aa38dd42b1..46cbc2a8a2 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -13,14 +13,29 @@ configuration options for TechDocs. # File: app-config.yaml techdocs: - # generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to - # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). - # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running - # into Docker in Docker situation. Read more here - # https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional + # techdocs.generator is used to configure how documentation sites are generated using MkDocs. - generators: - techdocs: 'docker' + generator: + # techdocs.generator.runIn can be either 'docker' or 'local'. This is to determine how to run the generator - whether to + # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). + # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running + # into Docker in Docker situation. Read more here + # https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional + + runIn: 'docker' + + # (Optional) techdocs.generator.dockerImage can be used to control the docker image used during documentation generation. This can be useful + # if you want to use MkDocs plugins or other packages that are not included in the default techdocs-container (spotify/techdocs). + # NOTE: This setting is only used when techdocs.generator.runIn is set to 'docker'. + + dockerImage: 'spotify/techdocs' + + # (Optional) techdocs.generator.pullImage can be used to disable pulling the latest docker image by default. This can be useful when you are + # using a custom techdocs.generator.dockerImage and you have a custom docker login requirement. For example, you need to login to + # AWS ECR to pull the docker image. + # NOTE: Disabling this requires the docker image was pulled by other means before running the techdocs generator. + + pullImage: true # techdocs.builder can be either 'local' or 'external. # If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage @@ -98,6 +113,14 @@ techdocs: # https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json accountKey: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY} + # (Optional and not recommended) Prior to version [0.x.y] of TechDocs, docs + # sites could only be accessed over paths with case-sensitive entity triplets + # e.g. (namespace/Kind/name). If you are upgrading from an older version of + # TechDocs and are unable to perform the necessary migration of files in your + # external storage, you can set this value to `true` to temporarily revert to + # the old, case-sensitive entity triplet behavior. + legacyUseCaseSensitiveTripletPaths: false + # (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc. # You don't have to specify this anymore. diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 3fdc942ac4..93ce3ad4d9 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -66,9 +66,7 @@ Update your component's entity description by adding the following lines to its ```yaml metadata: annotations: - backstage.io/techdocs-ref: url:https://github.com/org/repo - # Or - # backstage.io/techdocs-ref: url:https://github.com/org/repo/tree/branchName/subFolder + backstage.io/techdocs-ref: dir:. ``` The diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index cf0bbb0de1..0b1809dc22 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -33,14 +33,24 @@ In `packages/app/src/App.tsx`, import `TechDocsPage` and add the following to `FlatRoutes`: ```tsx -import { TechDocsPage } from '@backstage/plugin-techdocs'; +import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; // ... const AppRoutes = () => { // ... other plugin routes - } /> + }> + + + } + /> ; }; ``` @@ -204,7 +214,7 @@ techdocs: builder: 'local' publisher: type: 'local' - generators: + generator: techdocs: local ``` diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 3f3c92d0de..326ac02969 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -35,38 +35,64 @@ In your Backstage instance's `app-config.yaml`, set `techdocs.builder` from `'local'` to `'external'`. By doing this, TechDocs will not try to generate docs. Look at [TechDocs configuration](configuration.md) for reference. -## How to use URL Reader in TechDocs Prepare step? +## How to understand techdocs-ref annotation values -If TechDocs is configured to generate docs, it will first download the -repository associated with the `backstage.io/techdocs-ref` annotation defined in -the Entity's `catalog-info.yaml` file. This is also called the +If TechDocs is configured to generate docs, it will first download source files +based on the value of the `backstage.io/techdocs-ref` annotation defined in the +Entity's `catalog-info.yaml` file. This is also called the [Prepare](./concepts.md#techdocs-preparer) step. -There are two kinds of preparers or two ways of downloading these source files +We strongly recommend that the `backstage.io/techdocs-ref` annotation in each +documented catalog entity's `catalog-info.yaml` be set to `dir:.` in almost all +situations. This is because TechDocs is aligned with the "docs like code" +philosophy, whereby documentation should be authored and managed alongside the +source code of the underlying software itself. -- Preparer 1: Doing a `git clone` of the repository (also known as Common Git - Preparer) -- Preparer 2: Downloading an archive.zip or equivalent of the repository (also - known as URL Reader) +When you see `dir:.`, you can translate it to mean: -If `backstage.io/techdocs-ref` is equal to any of these - +- That the documentation source code lives in the same location as the + `catalog-info.yaml` file. +- That, in particular, the `mkdocs.yml` file is a sibling of `catalog-info.yaml` + (meaning, it is in the same directory) +- And that all of the source content of the documentation would be available if + one were to download the directory containing those two files (as well as all + sub-directories). -1. `github:https://githubhost.com/org/repo` -2. `gitlab:https://gitlabhost.com/org/repo` -3. `bitbucket:https://bitbuckethost.com/project/repo` -4. `azure/api:https://azurehost.com/org/project` +The directory tree of the entity would look something like this: -Then Common Git Preparer will be used i.e. a `git clone`. But the URL Reader is -a much faster way to do this step. Convert the `backstage.io/techdocs-ref` -values to the following - +``` +├── catalog-info.yaml +├── mkdocs.yml +└── docs + └── index.md +``` -1. `url:https://githubhost.com/org/repo/tree/` -2. `url:https://gitlabhost.com/org/repo/tree/` -3. `url:https://bitbuckethost.com/project/repo/src/` -4. `url:https://azurehost.com/organization/project/_git/repository` +If, for example, you wanted to keep a lean root directory, you could place your +`mkdocs.yml` file in a subdirectory and update the `backstage.io/techdocs-ref` +annotation value accordingly, e.g. to `dir:./sub-folder`: -Note that you can also provide a path to a non-root directory inside the -repository which contains the `docs/` directory. +``` +├── catalog-info.yaml +└── sub-folder + ├── mkdocs.yml + └── docs + └── index.md +``` + +In rare situations where your TechDocs source content is managed and stored in a +location completely separate from your `catalog-info.yaml`, you can instead +specify a URL location reference, the exact value of which will vary based on +the source code hosting provider. Notice that instead of the `dir:` prefix, the +`url:` prefix is used instead. For example: + +- **GitHub**: `url:https://githubhost.com/org/repo/tree/` +- **GitLab**: `url:https://gitlabhost.com/org/repo/tree/` +- **Bitbucket**: `url:https://bitbuckethost.com/project/repo/src/` +- **Azure**: `url:https://azurehost.com/organization/project/_git/repository` + +Note, just as it's possible to specify a subdirectory with the `dir:` prefix, +you can also provide a path to a non-root directory inside the repository which +contains the `mkdocs.yml` file and `docs/` directory. e.g. `url:https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/examples/documented-component` @@ -78,83 +104,116 @@ the repository. The archive does not have any git history attached to it. Also it is a compressed file. Hence the file size is significantly smaller than how much data git clone has to transfer. -## How to use a custom TechDocs home page? +## How to customize the TechDocs home page? -### 1st way: TechDocsCustomHome with a custom configuration +TechDocs uses a composability pattern similar to the Search and Catalog plugins +in Backstage. While a default table experience, similar to the one provided by +the Catalog plugin, is made available for ease-of-use, it's possible for you to +provide a completely custom experience, tailored to the needs of your +organization. -As an example, in your main App.tsx: +This is done in your `app` package. By default, you might see something like +this in your `App.tsx`: ```tsx -import { - TechDocsCustomHome, - PanelType, - TechDocsReaderPage, -} from '@backstage/plugin-techdocs'; -import { Entity } from '@backstage/catalog-model'; - -const tabsConfig = [ - { - label: 'Custom Tab', - panels: [ - { - title: 'Custom Documents Cards 1', - description: - 'Explore your internal technical ecosystem through documentation.', - panelType: 'DocsCardGrid' as PanelType, - // optional, is applied to a container of the panel (excludes header of panel) - panelCSS: { maxHeight: '400px', overflow:'auto' }, - filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationOne']; - }, - { - title: 'Custom Documents Cards 2', - description: - 'Explore your internal technical ecosystem through documentation.', - panelType: 'DocsCardGrid' as PanelType, - panelCSS: { maxHeight: '400px', overflow:'auto' }, - filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationTwo']; - }, - ], - }, - { - label: 'Overview', - panels: [ - { - title: 'Overview', - description: - 'Explore your internal technical ecosystem through documentation.', - panelType: 'DocsTable' as PanelType, - filterPredicate: () => true, - }, - ], - }, -]; - -const routes = ( +const AppRoutes = () => { - } - /> - } - /> - + }> + + + ; +}; ``` -An example of tabsConfig that corresponds to the default documentation home page -can be found at `plugins/techdocs/src/home/components/TechDocsHome.tsx`. +But you can replace `` with any React component, which +will be rendered in its place. Most likely, you would want to create and +maintain such a component in a new directory at +`packages/app/src/components/techdocs`, and import and use it in `App.tsx`: -Currently `panelType` has DocsCardGrid and DocsTable available. We currently -recommend that DocsCardGrid can be optionally vertically stacked by setting a -maxHeight using `panelCSS`, and DocsTable to be in a tab by itself. +```tsx +import { CustomTechDocsHome } from './components/techdocs/CustomTechDocsHome'; +// ... +const AppRoutes = () => { + + }> + + + ; +}; +``` -### 2nd way: Custom home page plugin +## How to migrate from TechDocs Alpha to Beta -A custom home page plugin can be built that uses the components extensions -DocsCardGrid and DocsTable, exported from @backstage/techdocs. They both take a -array of documentation entities ( i.e.have a 'backstage.io/techdocs-ref' -annotation ) as an 'entities' attribute. +> This guide only applies to the "recommended" TechDocs deployment method (where +> an external storage provider and external CI/CD is used). If you use the +> "basic" or "out-of-the-box" setup, you can stop here! No action needed. -For a reference to the React structure of the default home page, please refer to -`plugins/techdocs/src/home/components/TechDocsCustomHome.tsx`. +For the purposes of this guide, TechDocs Beta version is defined as: + +- **TechDocs Plugin**: At least `v0.11.0` +- **TechDocs Backend Plugin**: At least `v0.10.0` +- **TechDocs CLI**: At least `v0.7.0` + +The beta version of TechDocs made a breaking change to the way TechDocs content +was accessed and stored, allowing pages to be accessed with case-insensitive +entity triplet paths (e.g. `/docs/namespace/kind/name` whereas in prior +versions, they could only be accessed at `/docs/namespace/Kind/name`). In order +to enable this change, documentation has to be stored in an external storage +provider using an object key whose entity triplet is lower-cased. + +New installations of TechDocs since the beta version will work fine with no +action, but for those who were running TechDocs prior to this version, a +migration will need to be performed so that all existing content in your storage +bucket matches this lower-case entity triplet expectation. + +1. **Ensure you have the right permissions on your storage provider**: In order + to migrate files in your storage provider, the `techdocs-cli` needs to be + able to read/copy/rename/move/delete files. The exact instructions vary by + storage provider, but check the [using cloud storage][using-cloud-storage] + page for details. + +2. **Run a non-destructive migration of files**: Ensure you have the latest + version of `techdocs-cli` installed. Then run the following command, using + the details relevant for your provider / configuration. This will copy all + files from, e.g. `namespace/Kind/name/index.html` to + `namespace/kind/name/index.html`, without removing the original files. + +```sh +techdocs-cli migrate --publisher-type --storage-name --verbose +``` + +3. **Deploy the updated versions of the TechDocs plugins**: Once the migration + above has been run, you can deploy the beta versions of the TechDocs backend + and frontend plugins to your Backstage instance. + +4. **Verify that your TechDocs sites are still loading/accessible**: Try + accessing a TechDocs site using different entity-triplet case variants, e.g. + `/docs/namespace/KIND/name` or `/docs/namespace/kind/name`. Your TechDocs + site should load regardless of the URL path casing you use. + +5. **Clean up the old objects from storage**: Once you've verified that your + TechDocs site is accessible, you can clean up your storage bucket by + re-running the `migrate` command on the TechDocs CLI, but with an additional + `removeOriginal` flag passed: + +```sh +techdocs-cli migrate --publisher-type --storage-name --removeOriginal --verbose +``` + +6. **Update your CI/CD pipelines to use the beta version of the TechDocs CLI**: + Finally, you can update all of your CI/CD pipelines to use at least v0.x.y of + the TechDocs CLI, ensuring that all sites are published to the new, + lower-cased entity triplet paths going forward. + +If you encounter problems running this migration, please [report the +issue][beta-migrate-bug]. You can temporarily revert to pre-beta storage +expectations with a configuration change: + +```yaml +techdocs: + legacyUseCaseSensitiveTripletPaths: true +``` + +[beta-migrate-bug]: +https://github.com/backstage/backstage/issues/new?assignees=&labels=bug&template=bug_template.md&title=[TechDocs]%20Unable%20to%20run%20beta%20migration +[using-cloud-storage]: ./using-cloud-storage.md diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 119629915e..66eaf00663 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -61,7 +61,7 @@ If you do not prefer (3a) and optionally like to use a service account, you can follow these steps. Create a new Service Account and a key associated with it. In roles of the -service account, use "Storage Admin". +service account, use "Storage Object Admin". If you want to create a custom role, make sure to include both `get` and `create` permissions for both "Objects" and "Buckets". See @@ -143,6 +143,8 @@ permissions to: - `s3:ListBucket` to retrieve bucket metadata - `s3:PutObject` to upload files to the bucket +- `s3:DeleteObject` and `s3:DeleteObjectVersion` to delete stale content during + re-publishing To _read_ TechDocs from the S3 bucket the IAM policy needs to have at a minimum permissions to: @@ -150,6 +152,39 @@ permissions to: - `s3:ListBucket` - To retrieve bucket metadata - `s3:GetObject` - To retrieve files from the bucket +> Note: If you need to migrate documentation objects from an older-style path +> format including case-sensitive entity metadata, you will need to add some +> additional permissions to be able to perform the migration, including: +> +> - `s3:PutBucketAcl` (for copying files, +> [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html)) +> - `s3:DeleteObject` and `s3:DeleteObjectVersion` (for deleting migrated files, +> [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html)) +> +> ...And you will need to ensure the permissions apply to the bucket itself, as +> well as all resources under the bucket. See the example policy below. + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TechDocsWithMigration", + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObjectVersion", + "s3:ListBucket", + "s3:DeleteObject", + "s3:PutObjectAcl" + ], + "Resource": ["arn:aws:s3:::your-bucket", "arn:aws:s3:::your-bucket/*"] + } + ] +} +``` + **4a. (Recommended) Setup authentication the AWS way, using environment variables** @@ -312,6 +347,10 @@ techdocs: accountKey: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY} ``` +In either case, the account or credentials used to access your container and all +TechDocs objects underneath it should have the `Storage Blog Data Owner` role +applied, in order to read, write, and delete objects as needed. + **4. That's it!** Your Backstage app is now ready to use Azure Blob Storage for TechDocs, to store @@ -360,9 +399,34 @@ techdocs: Set the configs in your `app-config.yaml` to point to your container name. -https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail#password-authentication-with-unscoped-authorization +https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail,authenticating-with-an-application-credential-detail#authenticating-with-an-application-credential for more details. +```yaml +techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + id: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_ID} + secret: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_SECRET} + authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} + swiftUrl: ${OPENSTACK_SWIFT_STORAGE_SWIFT_URL} +``` + +**4. That's it!** + +Your Backstage app is now ready to use OpenStack Swift Storage for TechDocs, to +store and read the static generated documentation files. When you start the +backend of the app, you should be able to see +`techdocs info Successfully connected to the OpenStack Swift Storage container` +in the logs. + +## Bonus: Migration from old OpenStack Swift Configuration + +Let's assume we have the old OpenStack Swift configuration here. + ```yaml techdocs: publisher: @@ -379,10 +443,40 @@ techdocs: region: ${OPENSTACK_SWIFT_STORAGE_REGION} ``` -**4. That's it!** +##### Step 1: Change the credential keys -Your Backstage app is now ready to use OpenStack Swift Storage for TechDocs, to -store and read the static generated documentation files. When you start the -backend of the app, you should be able to see -`techdocs info Successfully connected to the OpenStack Swift Storage container` -in the logs. +Since the new SDK uses _Application Credentials_ to authenticate OpenStack, we +need to change the keys `credentials.username` to `credentials.id`, +`credentials.password` to `credentials.secret` and use Application Credential ID +and secret here. For more detail about credentials look +[here](https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail,authenticating-with-an-application-credential-detail#authenticating-with-an-application-credential). + +##### Step 2: Remove the unused keys + +Since the new SDK doesn't use the old way authentication, we don't need the keys +`openStackSwift.keystoneAuthVersion`, `openStackSwift.domainId`, +`openStackSwift.domainName` and `openStackSwift.region`. So you can remove them. + +##### Step 3: Add Swift URL + +The new SDK needs the OpenStack Swift connection URL for connecting the Swift. +So you need to add a new key called `openStackSwift.swiftUrl` and give the +OpenStack Swift url here. Example url should look like that: +`https://example.com:6780/swift/v1` + +##### That's it! + +Your new configuration should look like that! + +```yaml +techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + id: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_ID} + secret: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_SECRET} + authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} + swiftUrl: ${OPENSTACK_SWIFT_STORAGE_SWIFT_URL} +``` diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index aa3851463f..862c3c0cb6 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -35,7 +35,7 @@ If you want more control over the theme, and for example customize font sizes and margins, you can use the lower-level `createThemeOverrides` function exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme) in combination with -[createMuiTheme](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme) +[createTheme](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme) from [@material-ui/core](https://www.npmjs.com/package/@material-ui/core). See the [@backstage/theme source](https://github.com/backstage/backstage/tree/master/packages/theme/src) @@ -128,7 +128,7 @@ const themeOptions = createThemeOptions({ tool: genPageTheme(['#123456','#123456'], shapes.round), service: genPageTheme(['#123456','#123456'], shapes.wave), website: genPageTheme(['#123456','#123456'], shapes.wave), - library: genPageTheme(['#123456','#123456'] shapes.wave), + library: genPageTheme(['#123456','#123456'], shapes.wave), other: genPageTheme(['#123456','#123456'], shapes.wave), app: genPageTheme(['#123456','#123456'], shapes.wave), apis: genPageTheme(['#123456','#123456'], shapes.wave), diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 13b52181df..b01afac5c3 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -24,11 +24,9 @@ an easier path to make Pull Requests. Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have [Node.js](https://nodejs.org/en/download/) Active LTS Release installed -(currently v14), [Yarn](https://classic.yarnpkg.com/en/docs/install) and -[Python](https://www.python.org/downloads/) (although you likely have it -already). You will also need to have -[Docker](https://docs.docker.com/engine/install/) installed to use some features -like Software Templates and TechDocs. +(currently v14) and [Yarn](https://classic.yarnpkg.com/en/docs/install). You +will also need to have [Docker](https://docs.docker.com/engine/install/) +installed to use some features like Software Templates and TechDocs. Using `npx` you can then run the following to create an app in a chosen subdirectory of your current working directory: diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index 1d5d58c2c9..bf2cc93ea5 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -147,11 +147,6 @@ are separated out into their own folder, see further down. Helps you setup a plugin for isolated development so that it can be served separately. -- [`docgen/`](https://github.com/backstage/backstage/tree/master/packages/docgen) - - Uses the - [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) - to read out definitions and generate documentation for it. - - [`e2e-test/`](https://github.com/backstage/backstage/tree/master/packages/e2e-test) - Another CLI that can be run to try out what would happen if you build all the packages, publish them, create a new app, and then run them. CI uses this for diff --git a/docs/integrations/aws-s3/locations.md b/docs/integrations/aws-s3/locations.md new file mode 100644 index 0000000000..fe1c6050c2 --- /dev/null +++ b/docs/integrations/aws-s3/locations.md @@ -0,0 +1,42 @@ +--- +id: locations +sidebar_label: Locations +title: Amazon Web Services S3 Locations +# prettier-ignore +description: Setting up an integration with Amazon Web Services S3 +--- + +The AWS S3 integration supports loading catalog entities from an S3 Bucket. +Entities can be added to +[static catalog configuration](../../features/software-catalog/configuration.md), +or registered with the +[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import) +plugin. + +## Configuration + +To use this integration, add configuration to your `app-config.yaml`: + +```yaml +integrations: + awsS3: + - host: amazonaws.com + accessKeyId: ${AWS_ACCESS_KEY_ID} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY} +``` + +Then make sure the environment variables `AWS_ACCESS_KEY_ID` and +`AWS_SECRET_ACCESS_KEY` are set when you run Backstage. + +Users with multiple AWS accounts may want to use a role for S3 storage that is +in a different AWS account. Using the `roleArn` parameter as seen below, you can +instruct the AWS S3 reader to assume a role before accessing S3: + +```yaml +integrations: + awsS3: + - host: amazonaws.com + accessKeyId: ${AWS_ACCESS_KEY_ID} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY} + roleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/example-role' +``` diff --git a/docs/integrations/bitbucket/discovery.md b/docs/integrations/bitbucket/discovery.md index fadb7c9f06..708acd8db2 100644 --- a/docs/integrations/bitbucket/discovery.md +++ b/docs/integrations/bitbucket/discovery.md @@ -38,7 +38,11 @@ The target is composed of four parts: repositories prefixed with `service-`. - The path within each repository to find the catalog YAML file. This will usually be `/catalog-info.yaml` or a similar variation for catalog files - stored in the root directory of each repository. + stored in the root directory of each repository. If omitted, the default value + `catalog-info.yaml` will be used. E.g. given that `my-project`and `service-a` + exists, `https://bitbucket.mycompany.com/projects/my-project/repos/service-*/` + will result in: + `https://bitbucket.mycompany.com/projects/my-project/repos/service-a/catalog-info.yaml`. ## Custom repository processing @@ -52,13 +56,11 @@ matching repository is processed. repository. ```typescript -const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({ - client, - repository, -}) { - // Custom logic for interpret the matching repository. - // See defaultRepositoryParser for an example -}; +const customRepositoryParser: BitbucketRepositoryParser = + async function* customRepositoryParser({ client, repository }) { + // Custom logic for interpret the matching repository. + // See defaultRepositoryParser for an example + }; const processor = BitbucketDiscoveryProcessor.fromConfig(env.config, { parser: customRepositoryParser, diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md new file mode 100644 index 0000000000..ade575b74d --- /dev/null +++ b/docs/integrations/gitlab/discovery.md @@ -0,0 +1,36 @@ +--- +id: discovery +title: GitLab Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from repositories in GitLab +--- + +The GitLab integration has a special discovery processor for discovering catalog +entities from GitLab. The processor will crawl the GitLab instance and register +entities matching the configured path. This can be useful as an alternative to +static locations or manually adding things to the catalog. + +To use the discovery processor, you'll need a GitLab integration +[set up](locations.md) with a `token`. Then you can add a location target to the +catalog configuration: + +```yaml +catalog: + locations: + - type: gitlab-discovery + target: https://gitlab.com/group/subgroup/blob/main/catalog-info.yaml +``` + +Note the `gitlab-discovery` type, as this is not a regular `url` processor. + +The target is composed of three parts: + +- The base URL, `https://gitlab.com` in this case +- The group path, `group/subgroup` in this case. This is optional: If you omit + this path the processor will scan the entire GitLab instance instead. +- The path within each repository to find the catalog YAML file. This will + usually be `/blob/main/catalog-info.yaml`, `/blob/master/catalog-info.yaml` or + a similar variation for catalog files stored in the root directory of each + repository. If you want to use the repository's default branch use the `*` + wildcard, e.g.: `/blob/*/catalog-info.yaml` diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 0013c99c6b..fa34ca3d9a 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -1,116 +1,185 @@ --- id: roadmap -title: Project roadmap -description: Roadmap of Backstage Project +title: Roadmap +description: Roadmap of Backstage --- -## Current status +## The Backstage Roadmap -> Backstage is currently under rapid development. This means that you can expect -> APIs and features to evolve. It is also recommended that teams who adopt -> Backstage today [upgrade their installation](../cli/commands.md#versionsbump) -> as new [releases](https://github.com/backstage/backstage/releases) become -> available, as Backwards compatibility is not yet guaranteed. +Backstage is currently under rapid development. This page details the project’s +public roadmap, the result of ongoing collaboration between the core maintainers +and the broader Backstage community. Treat the roadmap as an ever-evolving guide +to keep us aligned as a community on: -## Phases +- Upcoming enhancements and benefits, +- Planning contributions and support, +- Planning the project’s adoption, +- Understanding what things are coming soon, +- Avoiding duplication of work -We have divided the project into three high-level _phases_: +### How to influence the roadmap -- 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to - easily create a single consistent UI layer for your internal infrastructure - and tools. A set of reusable - [UX patterns and components](https://backstage.io/storybook) help ensure a - consistent experience between tools. +As we evolve Backstage, we want you to contribute actively in the journey to +define the most effective developer experience in the world. -- 🐢 **Phase 2:** Software Catalog - ([alpha released](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)) - - With a single catalog, Backstage makes it easy for a team to manage ten - services — and makes it possible for your company to manage thousands of them. +A roadmap is only useful if it captures real needs. If you have success stories, +feedback, or ideas, we want to hear from you! If you plan to work (or are +already working) on a new or existing feature, please let us know, so that we +can update the roadmap accordingly. We are also happy to share knowledge and +context that will help your feature land successfully. -- 🐇 **Phase 3:** Ecosystem (ongoing, see - [Plugin Marketplace](https://backstage.io/plugins)) - Everyone's - infrastructure stack is different. By fostering a vibrant community of - contributors we hope to provide an ecosystem of Open Source - plugins/integrations that allows you to pick the tools that match your stack. - -## Detailed roadmap - -If you have questions about the roadmap or want to provide feedback, we would -love to hear from you! Please create an -[Issue](https://github.com/backstage/backstage/issues/new/choose), ping us on -[Discord](https://discord.gg/EBHEGzX) or reach out directly at -[backstage-interest@spotify.com](mailto:backstage-interest@spotify.com). - -Want to help out? Awesome ❤️ Head over to +You can also head over to the [CONTRIBUTING](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) guidelines to get started. -### Ongoing work 🚧 +If you have specific questions about the roadmap, please create an +[issue](https://github.com/backstage/backstage/issues/new/choose), ping us on +[Discord](https://discord.gg/awD6SxgQ), or +[book time](http://calendly.com/spotify-backstage) with the Spotify team. -- **[Platform stabilization](https://github.com/backstage/backstage/milestone/19)** - - Stabilize the core of Backstage, including its core features, so that the - platform can be depended on for production use. After this, plugins will - require little-to-no maintenance. +### How to read the roadmap -- **[Kubernetes plugin for service owners](https://github.com/backstage/backstage/issues/2857)** - - Improve native support for Kubernetes, making it easier for service owners to - see and manage their services running in K8s, regardless if that's locally, in - AWS, GCS, Azure, or elsewhere. +The Backstage roadmap lays out both [“what’s next”](#whats-next) and +[“future work”](#future-work). With "next" we mean features planned for release +within the ongoing quarter starting in July until September 2021 included. With +"future" we mean features in the radar, but not yet scheduled. -- **[Search platform](../features/search/README.md)** - Evolve the basic search - functionality currently available into a platform that **a)** enables search - across the software catalog, TechDocs, and any other information exposed by - plugins, and **b)** supports a variety of search engine technologies. +The long-term roadmap (12 - 36 months) is not detailed in the public roadmap. +Third-party contributions are also not currently included in the roadmap. Let us +know about any ongoing developments and we’re happy to include it here as well. -- **[Software Templates V2](https://github.com/backstage/backstage/issues/2771)** - - Expand the templates to make the steps more composable by adding the ability - to add more steps for custom logic, including webhooks and using authorization - from integrations. +### Roadmap evolution -### Future work 🔮 +Will this roadmap change? Obviously! -- **Golden Path for Plugin Development** - Create an easy, standardized way for - developers to build plugins that will encourage contributions and lead to a - richer ecosystem for everyone. +Roadmap are always evolving and ours is no different; you can expect to see this +updated roughly every month. -- **[GraphQL API](https://github.com/backstage/backstage/milestone/13)** - A - GraphQL API will open up the rich metadata provided by Backstage in a single - query. Plugins can easily query this API as well as extend the model where - needed. +## What’s next -- **Inter-Plugin Communication** - **[Under consideration]** Establish more - clearly defined patterns for plugins to communicate. +The feature set below is planned for the ongoing quarter, and grouped by theme. +The list order doesn’t necessarily reflect priority, and the development/release +cycle will vary based on maintainer schedules. -- **Improved Access Control** - **[Under consideration]** Provide finer grained - access controls and management for better control of the platform user - experience. +### Backstage Core -### Plugins +The following features are planned for release: -Building and maintaining [plugins](https://backstage.io/plugins) is the work of -the entire Backstage community. +- **Composable homepage:** We’re seeing lots of interest from the community in + reusable components to build a homepage experience where users can easily + surface what they might find useful to start their tasks. Check out the + [milestone](https://github.com/backstage/backstage/milestone/34) for further + details. +- **Improved responsiveness:** Check out the + [RFC here](https://github.com/backstage/backstage/issues/6318) for further + details on how to improve the responsiveness for Backstage's UI. -A list of plugins that are in development is -[available here](https://github.com/backstage/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc). -We strongly recommend to upvote 👍 plugins you are interested in. This helps us -and the community prioritize what plugins to build. +### Software Templates -Are you missing a plugin for your favorite tool? Please -[suggest a new one](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). -Chances are that someone will jump in and help build it. +The following features are planned for release:: -### Community Initiatives 🧑‍🤝‍🧑 +- **Re-creation/resubmission in case of failure:** Speed up productivity by + allowing developers to relaunch a project after a failure or any unexpected + problem. In the current version, this task requires retyping and a full + re-creation from scratch. +- **Performance and usability improvements for contributors:** Reach a relevant + improvement in templating's performance through the replacement of + [handlebars](https://handlebarsjs.com/). Other replacements will be considered + as part of this task (possibly + [cookiecutter](https://cookiecutter.readthedocs.io/)) for easier software + template creation, allowing more contributors to reach their goals without + having to learn new tooling. +- **Improved extensibility through inclusion:** Make software templates more + maintainable and extensible by adding `$include` support for parameters. +- **Authenticated job creation:** Created jobs will be able to run with an + authenticated user with all actions tracked for future consumption and + evidence. Track users creating jobs and make “jobs created by me” reporting + available. -- [**Backstage Community Sessions**](https://github.com/backstage/community#meetups) - - A monthly meetup for the community to come together to share and learn about - the latest happenings in Backstage. +### Software Catalog -- **Backstage Hackathons** - (Coming soon) Open to everyone in our Backstage - community, a celebration of you, the project and building awesome things - together +The following features are planned for release: -### Completed milestones ✅ +- **Request For Comments (RFC) for composability improvements (routing):** + Enable plugins to be auto-added and make plugin installation and upgrades + easier for all Backstage users. This includes information card layouts, entity + pages containing content and hooking the external header, considering the + support of a separate deployment, and configuration for plugins. +- **Removing duplicated entities in catalog:** As any adopter knows, a software + catalog can contain thousands or more entities and it is very important to + avoid duplications in naming to prevent failures. With this development task, + two entities with the same name won't be allowed as described + [here](https://github.com/backstage/backstage/issues/4760). +- **Connecting identity to ownership to prepare for role-based access control + ([RBAC](https://en.wikipedia.org/wiki/Role-based_access_control)):** This is a + first step to supporting RBAC for the software catalog (see the + [future work section](#future-work) for further details). Provide each entity + within the software catalog with a recognized owner. +- **Catalog performance improvements through improved caching:** Fix the + performance gaps in the catalog processor, which currently doesn’t have a + strong caching mechanism. The current version often requires fetching a + relevant amount of data, especially at scale. +### Search + +The following features are planned for release: + +- ElasticSearch integration: Add ElasticSearch to the Search Platform as the + underlying search engine. Check out the + [milestone here](https://github.com/backstage/backstage/milestone/27) for + further details. + +### TechDocs + +The following features are planned for release: + +- **TechDocs beta release:** Fix remaining bugs to get TechDocs to Beta. Check + out the [milestone here](https://github.com/backstage/backstage/milestone/29) + for further details. + +## Future work + +The following feature list doesn’t represent a commitment to develop and the +list order doesn’t reflect any priority or importance. But these features are on +the maintainers’ radar, with clear interest expressed by the community. + +- **Improved UX design:** Provide a better Backstage user experience through + visual guidelines and templates, especially navigation across plug-ins and + portal functionalities. +- **Catalog composability (routing):** Follow up development after the RFC + planned for the ongoing quarter (see [what’s next](#whats-next) for further + details). +- **Catalog-import improvements:** Provide a faster (scalability) and better + (more features like move/rename) way to import entities into the Software + Catalog. Importing items in the Software Catalog is crucial for creating a + Backstage proof-of-concept or testing/planning for broader organizational + adoption. This enhancement better supports getting developers to use Backstage + with less effort and customization. +- **Catalog improvements:** Add pagination and sourcing to Software Catalog. +- **[GraphQL](https://graphql.org/) support:** Introduce the ability to query + Backstage backend services with a standard query language for APIs. +- **Software templates performance improvements through decoupling a separate + worker:** Improve performance through decoupling resource-consuming services + and making them asynchronous. In the current version, project auto-creation + through the Software Templating system can consume a lot of resources and + bottleneck many concurrent projects created simultaneously. +- **API discovery and documentation:** Add better support for the + [gRPC](https://grpc.io/). +- **Adding TechDocs search to the Search Platform:** Having this capability in + place will provide a better and new major version of the Search Platform + (v3.0). You can refer to the + [milestone here](https://github.com/backstage/backstage/milestone/28) for + further details. +- **TechDocs GA release:** Work toward enhancements necessary to get TechDocs to + general availability. Check out the + [milestone here](https://github.com/backstage/backstage/milestone/30) for + further details. + +## Completed milestones + +Read more about the completed (and released) features for reference. + +- [[Search] Out-of-the-Box Implementation (Alpha)](https://github.com/backstage/backstage/milestone/26) - [Deploy a product demo at `demo.backstage.io`](https://demo.backstage.io) - [Kubernetes plugin - v1](https://github.com/backstage/backstage/tree/master/plugins/kubernetes) - [Helm charts](https://github.com/backstage/backstage/tree/master/contrib/chart/backstage) diff --git a/docs/overview/stability-index.md b/docs/overview/stability-index.md index 2b730024fd..8e710d4e7c 100644 --- a/docs/overview/stability-index.md +++ b/docs/overview/stability-index.md @@ -145,12 +145,6 @@ Provides utilities for developing plugins in isolation. Stability: `0`. This package is largely broken and needs updates. -### `docgen` [GitHub](https://github.com/backstage/backstage/tree/master/packages/docgen/) - -Internal CLI utility for generating API Documentation. - -Stability: `N/A` - ### `e2e-test` [GitHub](https://github.com/backstage/backstage/tree/master/packages/e2e-test/) Internal CLI utility for running e2e tests. @@ -312,14 +306,6 @@ configuration. Stability: `1` -### `register-component` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/register-component/) - -A frontend plugin that allows the user to register entity locations in the -catalog. - -Stability: `0`. This plugin is likely to be replaced by a generic entity import -plugin instead. - ### `scaffolder` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/scaffolder/) The frontend scaffolder plugin where one can browse templates and initiate diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index 805e321093..f9f7590e09 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -89,6 +89,26 @@ integrations: - $include: example-backstage-app-credentials.yaml ``` +### Limiting the GitHub App installations + +If you want to limit the GitHub app installations visible to backstage you may +optionally include the `allowedInstallationOwners` option. + +```yaml +appId: 1 +allowedInstallationOwners: ['GlobexCorp'] +clientId: client id +clientSecret: client secret +webhookSecret: webhook secret +privateKey: | + -----BEGIN RSA PRIVATE KEY----- + ...Key content... + -----END RSA PRIVATE KEY----- +``` + +This will result in backstage preventing the use of any installation that is not +within the allow list. + ### Permissions for pull requests These are the minimum permissions required for creating a pull request with diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index beffcdb313..83fcfbddba 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -82,8 +82,8 @@ export const ExamplePage = examplePlugin.provide( This is where the plugin is created and where it creates and exports extensions that can be imported and used the app. See reference docs for -[createPlugin](../reference/createPlugin.md) or introduction to the new -[Composability System](./composability.md). +[createPlugin](../reference/core-plugin-api.createplugin.md) or introduction to +the new [Composability System](./composability.md). ## Components @@ -99,15 +99,14 @@ You may tweak these components, rename them and/or replace them completely. ## Connecting the plugin to the Backstage app -There are three things needed for a Backstage app to start making use of a -plugin. +There are two things needed for a Backstage app to start making use of a plugin. 1. Add plugin as dependency in `app/package.json` 2. Import and use one or more plugin extensions, for example in `app/src/App.tsx`. -Luckily these three steps happen automatically when you create a plugin with the -Backstage CLI. +Luckily both of these steps happen automatically when you create a plugin with +the Backstage CLI. ## Talking to the outside world diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md deleted file mode 100644 index d550ba10a9..0000000000 --- a/docs/reference/createPlugin-feature-flags.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: createPlugin-feature-flags -title: createPlugin - feature flags -description: Documentation on createPlugin - feature flags ---- - -The `featureFlags` object passed to the `register` function makes it possible -for plugins to register Feature Flags in Backstage for users to opt into. You -can use this to split out logic in your code for manual A/B testing, etc. - -Here's a code sample: - -```typescript -import { createPlugin } from '@backstage/core-plugin-api'; - -export default createPlugin({ - id: 'plugin-name', - register({ featureFlags }) { - featureFlags.register('enable-example-feature'); - }, -}); -``` - -## Using with useApi - -To inspect the state of a feature flag inside your plugin, you can use the -`FeatureFlagsApi`, accessed via the `featureFlagsApiRef`. For example: - -```tsx -import React from 'react'; -import { Button } from '@material-ui/core'; -import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; - -const ExamplePage = () => { - const featureFlags = useApi(featureFlagsApiRef); - - return ( -
- - { featureFlags.isActive('enable-example-feature') && } -
- ); -}; -``` diff --git a/docs/reference/createPlugin.md b/docs/reference/createPlugin.md deleted file mode 100644 index 6602b0b2cc..0000000000 --- a/docs/reference/createPlugin.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: createPlugin -title: createPlugin -description: Documentation on createPlugin ---- - -Takes a plugin config as an argument and returns a new plugin. - -## Plugin Config - -```typescript -function createPlugin(config: PluginConfig): BackstagePlugin; - -type PluginConfig = { - id: string; - register?(hooks: PluginHooks): void; -}; - -type PluginHooks = { - featureFlags: FeatureFlagsHooks; -}; -``` - -- [Read more about feature flags here](createPlugin-feature-flags.md) - -## Example Uses - -### Creating a basic plugin - -Showcasing adding a feature flag. - -```jsx -import { createPlugin } from '@backstage/core-plugin-api'; - -export default createPlugin({ - id: 'new-plugin', - register({ router, featureFlags }) { - featureFlags.register('enable-example-component'); - }, -}); -``` diff --git a/docs/reference/utility-apis/AlertApi.md b/docs/reference/utility-apis/AlertApi.md deleted file mode 100644 index 8d1851cc44..0000000000 --- a/docs/reference/utility-apis/AlertApi.md +++ /dev/null @@ -1,114 +0,0 @@ -# AlertApi - -The AlertApi type is defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L29). - -The following Utility API implements this type: [alertApiRef](./README.md#alert) - -## Members - -### post() - -Post an alert for handling by the application. - -
-post(alert: AlertMessage): void
-
- -### alert\$() - -Observe alerts posted by other parts of the application. - -
-alert$(): Observable<AlertMessage>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AlertMessage - -
-export type AlertMessage = {
-  message: string;
-  // Severity will default to success since that is what material ui defaults the value to.
-  severity?: 'success' | 'info' | 'warning' | 'error';
-}
-
- -Defined at -[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). - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[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). - -### Observer - -This file contains non-react related core types used throughout Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[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). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[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 deleted file mode 100644 index a662b4cb70..0000000000 --- a/docs/reference/utility-apis/AppThemeApi.md +++ /dev/null @@ -1,271 +0,0 @@ -# AppThemeApi - -The AppThemeApi type is defined at -[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) - -## Members - -### getInstalledThemes() - -Get a list of available themes. - -
-getInstalledThemes(): AppTheme[]
-
- -### activeThemeId\$() - -Observe the currently selected theme. A value of undefined means no specific -theme has been selected. - -
-activeThemeId$(): Observable<string | undefined>
-
- -### getActiveThemeId() - -Get the current theme ID. Returns undefined if no specific theme is selected. - -
-getActiveThemeId(): string | undefined
-
- -### setActiveThemeId() - -Set a specific theme to use in the app, overriding the default theme selection. - -Clear the selection by passing in undefined. - -
-setActiveThemeId(themeId?: string): void
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AppTheme - -Describes a theme provided by the app. - -
-export type AppTheme = {
-  /**
-   * ID used to remember theme selections.
-   */
-  id: string;
-
-  /**
-   * Title of the theme
-   */
-  title: string;
-
-  /**
-   * Theme variant
-   */
-  variant: 'light' | 'dark';
-
-  /**
-   * The specialized MaterialUI theme instance.
-   */
-  theme: BackstageTheme;
-
-  /**
-   * An Icon for the theme mode setting.
-   */
-  icon?: React.ReactElement<SvgIconProps>;
-}
-
- -Defined at -[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). - -### BackstagePalette - -
-export type BackstagePalette = Palette & PaletteAdditions
-
- -Defined at -[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L74). - -Referenced by: [BackstageTheme](#backstagetheme). - -### BackstageTheme - -
-export interface BackstageTheme extends Theme {
-  palette: BackstagePalette;
-  page: PageTheme;
-  getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme;
-}
-
- -Defined at -[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L81). - -Referenced by: [AppTheme](#apptheme). - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[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). - -### Observer - -This file contains non-react related core types used throughout Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[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). - -### PageTheme - -
-export type PageTheme = {
-  colors: string[];
-  shape: string;
-  backgroundImage: string;
-}
-
- -Defined at -[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L103). - -Referenced by: [BackstageTheme](#backstagetheme). - -### PageThemeSelector - -
-export type PageThemeSelector = {
-  themeId: string;
-}
-
- -Defined at -[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L77). - -Referenced by: [BackstageTheme](#backstagetheme). - -### PaletteAdditions - -
-type PaletteAdditions = {
-  status: {
-    ok: string;
-    warning: string;
-    error: string;
-    pending: string;
-    running: string;
-    aborted: string;
-  };
-  border: string;
-  textContrast: string;
-  textVerySubtle: string;
-  textSubtle: string;
-  highlight: string;
-  errorBackground: string;
-  warningBackground: string;
-  infoBackground: string;
-  errorText: string;
-  infoText: string;
-  warningText: string;
-  linkHover: string;
-  link: string;
-  gold: string;
-  navigation: {
-    background: string;
-    indicator: string;
-    color: string;
-    selectedColor: string;
-  };
-  tabbar: {
-    indicator: string;
-  };
-  bursts: {
-    fontColor: string;
-    slackChannelText: string;
-    backgroundColor: {
-      default: string;
-    };
-  };
-  pinSidebarButton: {
-    icon: string;
-    background: string;
-  };
-  banner: {
-    info: string;
-    error: string;
-    text: string;
-    link: string;
-  };
-}
-
- -Defined at -[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L23). - -Referenced by: [BackstagePalette](#backstagepalette). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[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 deleted file mode 100644 index 80a40d427e..0000000000 --- a/docs/reference/utility-apis/BackstageIdentityApi.md +++ /dev/null @@ -1,100 +0,0 @@ -# BackstageIdentityApi - -The BackstageIdentityApi type is defined at -[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: - -- [auth0AuthApiRef](./README.md#auth0auth) - -- [githubAuthApiRef](./README.md#githubauth) - -- [gitlabAuthApiRef](./README.md#gitlabauth) - -- [googleAuthApiRef](./README.md#googleauth) - -- [microsoftAuthApiRef](./README.md#microsoftauth) - -- [oauth2ApiRef](./README.md#oauth2) - -- [oidcAuthApiRef](./README.md#oidcauth) - -- [oktaAuthApiRef](./README.md#oktaauth) - -- [oneloginAuthApiRef](./README.md#oneloginauth) - -- [samlAuthApiRef](./README.md#samlauth) - -## Members - -### getBackstageIdentity() - -Get the user's identity within Backstage. This should normally not be called -directly, use the @IdentityApi instead. - -If the optional flag is not set, a session is guaranteed to be returned, while -if the optional flag is set, the session may be undefined. See -@AuthRequestOptions for more details. - -
-getBackstageIdentity(
-    options?: AuthRequestOptions,
-  ): Promise<BackstageIdentity | undefined>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AuthRequestOptions - -
-export type AuthRequestOptions = {
-  /**
-   * If this is set to true, the user will not be prompted to log in,
-   * and an empty response will be returned if there is no existing session.
-   *
-   * This can be used to perform a check whether the user is logged in, or if you don't
-   * want to force a user to be logged in, but provide functionality if they already are.
-   *
-   * @default false
-   */
-  optional?: boolean;
-
-  /**
-   * If this is set to true, the request will bypass the regular oauth login modal
-   * and open the login popup directly.
-   *
-   * The method must be called synchronously from a user action for this to work in all browsers.
-   *
-   * @default false
-   */
-  instantPopup?: boolean;
-}
-
- -Defined at -[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). - -### BackstageIdentity - -
-export type BackstageIdentity = {
-  /**
-   * The backstage user ID.
-   */
-  id: string;
-
-  /**
-   * An ID token that can be used to authenticate the user within Backstage.
-   */
-  idToken: string;
-}
-
- -Defined at -[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 deleted file mode 100644 index c5b20610e7..0000000000 --- a/docs/reference/utility-apis/Config.md +++ /dev/null @@ -1,187 +0,0 @@ -# Config - -The Config type is defined at -[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) - -## Members - -### has() - -
-has(key: string): boolean
-
- -### keys() - -
-keys(): string[]
-
- -### get() - -
-get(key?: string): JsonValue
-
- -### getOptional() - -
-getOptional(key?: string): JsonValue | undefined
-
- -### getConfig() - -
-getConfig(key: string): Config
-
- -### getOptionalConfig() - -
-getOptionalConfig(key: string): Config | undefined
-
- -### getConfigArray() - -
-getConfigArray(key: string): Config[]
-
- -### getOptionalConfigArray() - -
-getOptionalConfigArray(key: string): Config[] | undefined
-
- -### getNumber() - -
-getNumber(key: string): number
-
- -### getOptionalNumber() - -
-getOptionalNumber(key: string): number | undefined
-
- -### getBoolean() - -
-getBoolean(key: string): boolean
-
- -### getOptionalBoolean() - -
-getOptionalBoolean(key: string): boolean | undefined
-
- -### getString() - -
-getString(key: string): string
-
- -### getOptionalString() - -
-getOptionalString(key: string): string | undefined
-
- -### getStringArray() - -
-getStringArray(key: string): string[]
-
- -### getOptionalStringArray() - -
-getOptionalStringArray(key: string): string[] | undefined
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### Config - -
-export type Config = {
-  has(key: string): boolean;
-
-  keys(): string[];
-
-  get(key?: string): JsonValue;
-  getOptional(key?: string): JsonValue | undefined;
-
-  getConfig(key: string): Config;
-  getOptionalConfig(key: string): Config | undefined;
-
-  getConfigArray(key: string): Config[];
-  getOptionalConfigArray(key: string): Config[] | undefined;
-
-  getNumber(key: string): number;
-  getOptionalNumber(key: string): number | undefined;
-
-  getBoolean(key: string): boolean;
-  getOptionalBoolean(key: string): boolean | undefined;
-
-  getString(key: string): string;
-  getOptionalString(key: string): string | undefined;
-
-  getStringArray(key: string): string[];
-  getOptionalStringArray(key: string): string[] | undefined;
-}
-
- -Defined at -[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), -[getOptionalConfigArray](#getoptionalconfigarray), [Config](#config). - -### JsonArray - -
-export type JsonArray = JsonValue[]
-
- -Defined at -[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L18). - -Referenced by: [JsonValue](#jsonvalue). - -### JsonObject - -
-export type JsonObject = { [key in string]?: JsonValue }
-
- -Defined at -[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L17). - -Referenced by: [JsonValue](#jsonvalue). - -### JsonValue - -
-export type JsonValue =
-  | JsonObject
-  | JsonArray
-  | number
-  | string
-  | boolean
-  | null
-
- -Defined at -[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 deleted file mode 100644 index 0d888c1daa..0000000000 --- a/docs/reference/utility-apis/DiscoveryApi.md +++ /dev/null @@ -1,24 +0,0 @@ -# DiscoveryApi - -The DiscoveryApi type is defined at -[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) - -## Members - -### getBaseUrl() - -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 -fetching the URL when constructing an API client. That is to ensure that more -flexible routing patterns can be supported. - -For example, asking for the URL for `auth` may return something like -`https://backstage.example.com/api/auth` - -
-getBaseUrl(pluginId: string): Promise<string>
-
diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md deleted file mode 100644 index 1aaecdb47e..0000000000 --- a/docs/reference/utility-apis/ErrorApi.md +++ /dev/null @@ -1,134 +0,0 @@ -# ErrorApi - -The ErrorApi type is defined at -[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) - -## Members - -### post() - -Post an error for handling by the application. - -
-post(error: Error, context?: ErrorContext): void
-
- -### error\$() - -Observe errors posted by other parts of the application. - -
-error$(): Observable<{ error: Error; context?: ErrorContext }>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### Error - -Mirrors the JavaScript Error class, for the purpose of providing documentation -and optional fields. - -
-type Error = {
-  name: string;
-  message: string;
-  stack?: string;
-}
-
- -Defined at -[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). - -### ErrorContext - -Provides additional information about an error that was posted to the -application. - -
-export type ErrorContext = {
-  // If set to true, this error should not be displayed to the user. Defaults to false.
-  hidden?: boolean;
-}
-
- -Defined at -[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). - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[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). - -### Observer - -This file contains non-react related core types used throughout Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[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). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[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 deleted file mode 100644 index 1e8979c02e..0000000000 --- a/docs/reference/utility-apis/FeatureFlagsApi.md +++ /dev/null @@ -1,113 +0,0 @@ -# FeatureFlagsApi - -The FeatureFlagsApi type is defined at -[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) - -## Members - -### registerFlag() - -Registers a new feature flag. Once a feature flag has been registered it can be -toggled by users, and read back to enable or disable features. - -
-registerFlag(flag: FeatureFlag): void
-
- -### getRegisteredFlags() - -Get a list of all registered flags. - -
-getRegisteredFlags(): FeatureFlag[]
-
- -### isActive() - -Whether the feature flag with the given name is currently activated for the -user. - -
-isActive(name: string): boolean
-
- -### save() - -Save the user's choice of feature flag states. - -
-save(options: FeatureFlagsSaveOptions): void
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### FeatureFlag - -The feature flags API is used to toggle functionality to users across plugins -and Backstage. - -Plugins can use this API to register feature flags that they have available for -users to enable/disable, and this API will centralize the current user's state -of which feature flags they would like to enable. - -This is ideal for Backstage plugins, as well as your own App, to trial -incomplete or unstable upcoming features. Although there will be a common -interface for users to enable and disable feature flags, this API acts as -another way to enable/disable. - -
-export type FeatureFlag = {
-  name: string;
-  pluginId: string;
-}
-
- -Defined at -[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). - -### FeatureFlagState - -
-export enum FeatureFlagState {
-  None = 0,
-  Active = 1,
-}
-
- -Defined at -[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). - -### FeatureFlagsSaveOptions - -Options to use when saving feature flags. - -
-export type FeatureFlagsSaveOptions = {
-  /**
-   * The new feature flag states to save.
-   */
-  states: Record<string, FeatureFlagState>;
-
-  /**
-   * Whether the saves states should be merged into the existing ones, or replace them.
-   *
-   * Defaults to false.
-   */
-  merge?: boolean;
-}
-
- -Defined at -[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 deleted file mode 100644 index aa2a4dd9a0..0000000000 --- a/docs/reference/utility-apis/IdentityApi.md +++ /dev/null @@ -1,81 +0,0 @@ -# IdentityApi - -The IdentityApi type is defined at -[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) - -## Members - -### getUserId() - -The ID of the signed in user. This ID is not meant to be presented to the user, -but used as an opaque string to pass on to backends or use in frontend logic. - -TODO: The intention of the user ID is to be able to tie the user to an identity -that is known by the catalog and/or identity backend. It should for example be -possible to fetch all owned components using this ID. - -
-getUserId(): string
-
- -### getProfile() - -The profile of the signed in user. - -
-getProfile(): ProfileInfo
-
- -### getIdToken() - -An OpenID Connect ID Token which proves the identity of the signed in user. - -The ID token will be undefined if the signed in user does not have a verified -identity, such as a demo user or mocked user for e2e tests. - -
-getIdToken(): Promise<string | undefined>
-
- -### signOut() - -Sign out the current user - -
-signOut(): Promise<void>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### ProfileInfo - -Profile information of the user. - -
-export type ProfileInfo = {
-  /**
-   * Email ID.
-   */
-  email?: string;
-
-  /**
-   * Display name that can be presented to the user.
-   */
-  displayName?: string;
-
-  /**
-   * URL to an avatar image of the user.
-   */
-  picture?: string;
-}
-
- -Defined at -[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 deleted file mode 100644 index 9af6b3bb53..0000000000 --- a/docs/reference/utility-apis/OAuthApi.md +++ /dev/null @@ -1,117 +0,0 @@ -# OAuthApi - -The OAuthApi type is defined at -[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: - -- [githubAuthApiRef](./README.md#githubauth) - -- [gitlabAuthApiRef](./README.md#gitlabauth) - -- [googleAuthApiRef](./README.md#googleauth) - -- [microsoftAuthApiRef](./README.md#microsoftauth) - -- [oauth2ApiRef](./README.md#oauth2) - -- [oidcAuthApiRef](./README.md#oidcauth) - -- [oktaAuthApiRef](./README.md#oktaauth) - -- [oneloginAuthApiRef](./README.md#oneloginauth) - -## Members - -### getAccessToken() - -Requests an OAuth 2 Access Token, optionally with a set of scopes. The access -token allows you to make requests on behalf of the user, and the copes may grant -you broader access, depending on the auth provider. - -Each auth provider has separate handling of scope, so you need to look at the -documentation for each one to know what scope you need to request. - -This method is cheap and should be called each time an access token is used. Do -not for example store the access token in React component state, as that could -cause the token to expire. Instead fetch a new access token for each request. - -Be sure to include all required scopes when requesting an access token. When -testing your implementation it is best to log out the Backstage session and then -visit your plugin page directly, as you might already have some required scopes -in your existing session. Not requesting the correct scopes can lead to 403 or -other authorization errors, which can be tricky to debug. - -If the user has not yet granted access to the provider and the set of requested -scopes, the user will be prompted to log in. The returned promise will not -resolve until the user has successfully logged in. The returned promise can be -rejected, but only if the user rejects the login request. - -
-getAccessToken(
-    scope?: OAuthScope,
-    options?: AuthRequestOptions,
-  ): Promise<string>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AuthRequestOptions - -
-export type AuthRequestOptions = {
-  /**
-   * If this is set to true, the user will not be prompted to log in,
-   * and an empty response will be returned if there is no existing session.
-   *
-   * This can be used to perform a check whether the user is logged in, or if you don't
-   * want to force a user to be logged in, but provide functionality if they already are.
-   *
-   * @default false
-   */
-  optional?: boolean;
-
-  /**
-   * If this is set to true, the request will bypass the regular oauth login modal
-   * and open the login popup directly.
-   *
-   * The method must be called synchronously from a user action for this to work in all browsers.
-   *
-   * @default false
-   */
-  instantPopup?: boolean;
-}
-
- -Defined at -[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). - -### OAuthScope - -This file contains declarations for common interfaces of auth-related APIs. The -declarations should be used to signal which type of authentication and -authorization methods each separate auth provider supports. - -For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, -would be declared as follows: - -const googleAuthApiRef = createApiRef({ ... }) - -An array of scopes, or a scope string formatted according to the auth provider, -which is typically a space separated list. - -See the documentation for each auth provider for the list of scopes supported by -each provider. - -
-export type OAuthScope = string | string[]
-
- -Defined at -[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 deleted file mode 100644 index 5f521cf288..0000000000 --- a/docs/reference/utility-apis/OAuthRequestApi.md +++ /dev/null @@ -1,233 +0,0 @@ -# OAuthRequestApi - -The OAuthRequestApi type is defined at -[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) - -## Members - -### createAuthRequester() - -A utility for showing login popups or similar things, and merging together -multiple requests for different scopes into one request that includes all -scopes. - -The passed in options provide information about the login provider, and how to -handle auth requests. - -The returned AuthRequester function is used to request login with new scopes. -These requests are merged together and forwarded to the auth handler, as soon as -a consumer of auth requests triggers an auth flow. - -See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. - -
-createAuthRequester<AuthResponse>(
-    options: AuthRequesterOptions<AuthResponse>,
-  ): AuthRequester<AuthResponse>
-
- -### authRequest\$() - -Observers pending auth requests. The returned observable will emit all current -active auth request, at most one for each created auth requester. - -Each request has its own info about the login provider, forwarded from the auth -requester options. - -Depending on user interaction, the request should either be rejected, or used to -trigger the auth handler. If the request is rejected, all pending AuthRequester -calls will fail with a "RejectedError". If a auth is triggered, and the auth -handler resolves successfully, then all currently pending AuthRequester calls -will resolve to the value returned by the onAuthRequest call. - -
-authRequest$(): Observable<PendingAuthRequest[]>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AuthProvider - -Information about the auth provider that we're requesting a login towards. - -This should be shown to the user so that they can be informed about what login -is being requested before a popup is shown. - -
-export type AuthProvider = {
-  /**
-   * Title for the auth provider, for example "GitHub"
-   */
-  title: string;
-
-  /**
-   * Icon for the auth provider.
-   */
-  icon: IconComponent;
-}
-
- -Defined at -[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). - -### AuthRequester - -Function used to trigger new auth requests for a set of scopes. - -The returned promise will resolve to the same value returned by the -onAuthRequest in the AuthRequesterOptions. Or rejected, if the request is -rejected. - -This function can be called multiple times before the promise resolves. All -calls will be merged into one request, and the scopes forwarded to the -onAuthRequest will be the union of all requested scopes. - -
-export type AuthRequester<AuthResponse> = (
-  scopes: Set<string>,
-) => Promise<AuthResponse>
-
- -Defined at -[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). - -### AuthRequesterOptions - -Describes how to handle auth requests. Both how to show them to the user, and -what to do when the user accesses the auth request. - -
-export type AuthRequesterOptions<AuthResponse> = {
-  /**
-   * Information about the auth provider, which will be forwarded to auth requests.
-   */
-  provider: AuthProvider;
-
-  /**
-   * Implementation of the auth flow, which will be called synchronously when
-   * trigger() is called on an auth requests.
-   */
-  onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
-}
-
- -Defined at -[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). - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[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). - -### Observer - -This file contains non-react related core types used throughout Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[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). - -### PendingAuthRequest - -An pending auth request for a single auth provider. The request will remain in -this pending state until either reject() or trigger() is called. - -Any new requests for the same provider are merged into the existing pending -request, meaning there will only ever be a single pending request for a given -provider. - -
-export type PendingAuthRequest = {
-  /**
-   * Information about the auth provider, as given in the AuthRequesterOptions
-   */
-  provider: AuthProvider;
-
-  /**
-   * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
-   */
-  reject: () => void;
-
-  /**
-   * Trigger the auth request to continue the auth flow, by for example showing a popup.
-   *
-   * Synchronously calls onAuthRequest with all scope currently in the request.
-   */
-  trigger(): Promise<void>;
-}
-
- -Defined at -[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). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[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 deleted file mode 100644 index 6d05af6189..0000000000 --- a/docs/reference/utility-apis/OpenIdConnectApi.md +++ /dev/null @@ -1,75 +0,0 @@ -# OpenIdConnectApi - -The OpenIdConnectApi type is defined at -[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: - -- [auth0AuthApiRef](./README.md#auth0auth) - -- [googleAuthApiRef](./README.md#googleauth) - -- [microsoftAuthApiRef](./README.md#microsoftauth) - -- [oauth2ApiRef](./README.md#oauth2) - -- [oidcAuthApiRef](./README.md#oidcauth) - -- [oktaAuthApiRef](./README.md#oktaauth) - -- [oneloginAuthApiRef](./README.md#oneloginauth) - -## Members - -### getIdToken() - -Requests an OpenID Connect ID Token. - -This method is cheap and should be called each time an ID token is used. Do not -for example store the id token in React component state, as that could cause the -token to expire. Instead fetch a new id token for each request. - -If the user has not yet logged in to Google inside Backstage, the user will be -prompted to log in. The returned promise will not resolve until the user has -successfully logged in. The returned promise can be rejected, but only if the -user rejects the login request. - -
-getIdToken(options?: AuthRequestOptions): Promise<string>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AuthRequestOptions - -
-export type AuthRequestOptions = {
-  /**
-   * If this is set to true, the user will not be prompted to log in,
-   * and an empty response will be returned if there is no existing session.
-   *
-   * This can be used to perform a check whether the user is logged in, or if you don't
-   * want to force a user to be logged in, but provide functionality if they already are.
-   *
-   * @default false
-   */
-  optional?: boolean;
-
-  /**
-   * If this is set to true, the request will bypass the regular oauth login modal
-   * and open the login popup directly.
-   *
-   * The method must be called synchronously from a user action for this to work in all browsers.
-   *
-   * @default false
-   */
-  instantPopup?: boolean;
-}
-
- -Defined at -[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 deleted file mode 100644 index 1a2f94d031..0000000000 --- a/docs/reference/utility-apis/ProfileInfoApi.md +++ /dev/null @@ -1,104 +0,0 @@ -# ProfileInfoApi - -The ProfileInfoApi type is defined at -[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: - -- [auth0AuthApiRef](./README.md#auth0auth) - -- [githubAuthApiRef](./README.md#githubauth) - -- [gitlabAuthApiRef](./README.md#gitlabauth) - -- [googleAuthApiRef](./README.md#googleauth) - -- [microsoftAuthApiRef](./README.md#microsoftauth) - -- [oauth2ApiRef](./README.md#oauth2) - -- [oidcAuthApiRef](./README.md#oidcauth) - -- [oktaAuthApiRef](./README.md#oktaauth) - -- [oneloginAuthApiRef](./README.md#oneloginauth) - -- [samlAuthApiRef](./README.md#samlauth) - -## Members - -### getProfile() - -Get profile information for the user as supplied by this auth provider. - -If the optional flag is not set, a session is guaranteed to be returned, while -if the optional flag is set, the session may be undefined. See -@AuthRequestOptions for more details. - -
-getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### AuthRequestOptions - -
-export type AuthRequestOptions = {
-  /**
-   * If this is set to true, the user will not be prompted to log in,
-   * and an empty response will be returned if there is no existing session.
-   *
-   * This can be used to perform a check whether the user is logged in, or if you don't
-   * want to force a user to be logged in, but provide functionality if they already are.
-   *
-   * @default false
-   */
-  optional?: boolean;
-
-  /**
-   * If this is set to true, the request will bypass the regular oauth login modal
-   * and open the login popup directly.
-   *
-   * The method must be called synchronously from a user action for this to work in all browsers.
-   *
-   * @default false
-   */
-  instantPopup?: boolean;
-}
-
- -Defined at -[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). - -### ProfileInfo - -Profile information of the user. - -
-export type ProfileInfo = {
-  /**
-   * Email ID.
-   */
-  email?: string;
-
-  /**
-   * Display name that can be presented to the user.
-   */
-  displayName?: string;
-
-  /**
-   * URL to an avatar image of the user.
-   */
-  picture?: string;
-}
-
- -Defined at -[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 deleted file mode 100644 index aefbb4b925..0000000000 --- a/docs/reference/utility-apis/README.md +++ /dev/null @@ -1,202 +0,0 @@ -# Backstage Core Utility APIs - -The following is a list of all Utility APIs defined by `@backstage/core`. They -are available to use by plugins and components, and can be accessed using the -`useApi` hook, also provided by `@backstage/core`. For more information, see -https://github.com/backstage/backstage/blob/master/docs/api/utility-apis.md. - -### alert - -Used to report alerts and forward them to the app - -Implemented type: [AlertApi](./AlertApi.md) - -ApiRef: -[alertApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L41) - -### appTheme - -API Used to configure the app theme, and enumerate options - -Implemented type: [AppThemeApi](./AppThemeApi.md) - -ApiRef: -[appThemeApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80) - -### auth0Auth - -Provides authentication towards Auth0 APIs - -Implemented types: [OpenIdConnectApi](./OpenIdConnectApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[auth0AuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L275) - -### config - -Used to access runtime configuration - -Implemented type: [Config](./Config.md) - -ApiRef: -[configApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ConfigApi.ts#L25) - -### discovery - -Provides service discovery of backend plugins - -Implemented type: [DiscoveryApi](./DiscoveryApi.md) - -ApiRef: -[discoveryApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) - -### error - -Used to report errors and forward them to the app - -Implemented type: [ErrorApi](./ErrorApi.md) - -ApiRef: -[errorApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) - -### featureFlags - -Used to toggle functionality in features across Backstage - -Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md) - -ApiRef: -[featureFlagsApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83) - -### githubAuth - -Provides authentication towards GitHub APIs - -Implemented types: [OAuthApi](./OAuthApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[githubAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L232) - -### gitlabAuth - -Provides authentication towards GitLab APIs - -Implemented types: [OAuthApi](./OAuthApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L262) - -### googleAuth - -Provides authentication towards Google APIs and identities - -Implemented types: [OAuthApi](./OAuthApi.md), -[OpenIdConnectApi](./OpenIdConnectApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[googleAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L215) - -### identity - -Provides access to the identity of the signed in user - -Implemented type: [IdentityApi](./IdentityApi.md) - -ApiRef: -[identityApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L53) - -### microsoftAuth - -Provides authentication towards Microsoft APIs and identities - -Implemented types: [OAuthApi](./OAuthApi.md), -[OpenIdConnectApi](./OpenIdConnectApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L289) - -### oauth2 - -Example of how to use oauth2 custom provider - -Implemented types: [OAuthApi](./OAuthApi.md), -[OpenIdConnectApi](./OpenIdConnectApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[oauth2ApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L303) - -### oauthRequest - -An API for implementing unified OAuth flows in Backstage - -Implemented type: [OAuthRequestApi](./OAuthRequestApi.md) - -ApiRef: -[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 - -Provides authentication towards Okta APIs - -Implemented types: [OAuthApi](./OAuthApi.md), -[OpenIdConnectApi](./OpenIdConnectApi.md), -[ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[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 - -Example of how to use SAML custom provider - -Implemented types: [ProfileInfoApi](./ProfileInfoApi.md), -[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) - -ApiRef: -[samlAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L331) - -### storage - -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/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 deleted file mode 100644 index 4b584d92c8..0000000000 --- a/docs/reference/utility-apis/SessionApi.md +++ /dev/null @@ -1,144 +0,0 @@ -# SessionApi - -The SessionApi type is defined at -[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: - -- [auth0AuthApiRef](./README.md#auth0auth) - -- [githubAuthApiRef](./README.md#githubauth) - -- [gitlabAuthApiRef](./README.md#gitlabauth) - -- [googleAuthApiRef](./README.md#googleauth) - -- [microsoftAuthApiRef](./README.md#microsoftauth) - -- [oauth2ApiRef](./README.md#oauth2) - -- [oidcAuthApiRef](./README.md#oidcauth) - -- [oktaAuthApiRef](./README.md#oktaauth) - -- [oneloginAuthApiRef](./README.md#oneloginauth) - -- [samlAuthApiRef](./README.md#samlauth) - -## Members - -### signIn() - -Sign in with a minimum set of permissions. - -
-signIn(): Promise<void>
-
- -### signOut() - -Sign out from the current session. This will reload the page. - -
-signOut(): Promise<void>
-
- -### sessionState\$() - -Observe the current state of the auth session. Emits the current state on -subscription. - -
-sessionState$(): Observable<SessionState>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[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). - -### Observer - -This file contains non-react related core types used throughout Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[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). - -### SessionState - -Session state values passed to subscribers of the SessionApi. - -
-export enum SessionState {
-  SignedIn = 'SignedIn',
-  SignedOut = 'SignedOut',
-}
-
- -Defined at -[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). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[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/SessionStateApi.md b/docs/reference/utility-apis/SessionStateApi.md deleted file mode 100644 index a8a4c3bc3d..0000000000 --- a/docs/reference/utility-apis/SessionStateApi.md +++ /dev/null @@ -1,119 +0,0 @@ -# SessionStateApi - -The SessionStateApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/backstage/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L201). - -The following Utility APIs implement this type: - -- [auth0AuthApiRef](./README.md#auth0auth) - -- [githubAuthApiRef](./README.md#githubauth) - -- [gitlabAuthApiRef](./README.md#gitlabauth) - -- [googleAuthApiRef](./README.md#googleauth) - -- [microsoftAuthApiRef](./README.md#microsoftauth) - -- [oauth2ApiRef](./README.md#oauth2) - -- [oktaAuthApiRef](./README.md#oktaauth) - -## Members - -### sessionState\$() - -
-sessionState$(): Observable<SessionState>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53). - -Referenced by: [sessionState\$](#sessionstate). - -### Observer - -This file contains non-react related core types used through Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24). - -Referenced by: [Observable](#observable). - -### SessionState - -Session state values passed to subscribers of the SessionStateApi. - -
-export enum SessionState {
-  SignedIn = 'SignedIn',
-  SignedOut = 'SignedOut',
-}
-
- -Defined at -[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/backstage/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L192). - -Referenced by: [sessionState\$](#sessionstate). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/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 deleted file mode 100644 index 3247d28f60..0000000000 --- a/docs/reference/utility-apis/StorageApi.md +++ /dev/null @@ -1,186 +0,0 @@ -# StorageApi - -The StorageApi type is defined at -[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) - -## Members - -### forBucket() - -Create a bucket to store data in. - -
-forBucket(name: string): StorageApi
-
- -### get() - -Get the current value for persistent data, use observe\$ to be notified of -updates. - -
-get<T>(key: string): T | undefined
-
- -### remove() - -Remove persistent data. - -
-remove(key: string): Promise<void>
-
- -### set() - -Save persistent data, and emit messages to anyone that is using observe\$ for -this key - -
-set(key: string, data: any): Promise<void>
-
- -### observe\$() - -Observe changes on a particular key in the bucket - -
-observe$<T>(key: string): Observable<StorageValueChange<T>>
-
- -## Supporting types - -These types are part of the API declaration, but may not be unique to this API. - -### Observable - -Observable sequence of values and errors, see TC39. - -https://github.com/tc39/proposal-observable - -This is used as a common return type for observable values and can be created -using many different observable implementations, such as zen-observable or -RxJS 5. - -
-export type Observable<T> = {
-  /**
-   * Subscribes to this observable to start receiving new values.
-   */
-  subscribe(observer: Observer<T>): Subscription;
-  subscribe(
-    onNext: (value: T) => void,
-    onError?: (error: Error) => void,
-    onComplete?: () => void,
-  ): Subscription;
-}
-
- -Defined at -[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). - -### Observer - -This file contains non-react related core types used throughout Backstage. - -Observer interface for consuming an Observer, see TC39. - -
-export type Observer<T> = {
-  next?(value: T): void;
-  error?(error: Error): void;
-  complete?(): void;
-}
-
- -Defined at -[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). - -### StorageApi - -
-export interface StorageApi {
-  /**
-   * Create a bucket to store data in.
-   * @param {String} name Namespace for the storage to be stored under,
-   *                      will inherit previous namespaces too
-   */
-  forBucket(name: string): StorageApi;
-
-  /**
-   * Get the current value for persistent data, use observe$ to be notified of updates.
-   *
-   * @param {String} key Unique key associated with the data.
-   * @return {Object} data The data that should is stored.
-   */
-  get<T>(key: string): T | undefined;
-
-  /**
-   * Remove persistent data.
-   *
-   * @param {String} key Unique key associated with the data.
-   */
-  remove(key: string): Promise<void>;
-
-  /**
-   * Save persistent data, and emit messages to anyone that is using observe$ for this key
-   *
-   * @param {String} key Unique key associated with the data.
-   */
-  set(key: string, data: any): Promise<void>;
-
-  /**
-   * Observe changes on a particular key in the bucket
-   * @param {String} key Unique key associated with the data
-   */
-  observe$<T>(key: string): Observable<StorageValueChange<T>>;
-}
-
- -Defined at -[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). - -### StorageValueChange - -
-export type StorageValueChange<T = any> = {
-  key: string;
-  newValue?: T;
-}
-
- -Defined at -[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). - -### Subscription - -Subscription returned when subscribing to an Observable, see TC39. - -
-export type Subscription = {
-  /**
-   * Cancels the subscription
-   */
-  unsubscribe(): void;
-
-  /**
-   * Value indicating whether the subscription is closed.
-   */
-  readonly closed: Boolean;
-}
-
- -Defined at -[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/lerna.json b/lerna.json index dd2dd884eb..322929db1d 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1" + "version": "0.1.0" } diff --git a/microsite/blog/2021-07-26-software-templates-are-now-in-beta.md b/microsite/blog/2021-07-26-software-templates-are-now-in-beta.md new file mode 100644 index 0000000000..b646fbdb22 --- /dev/null +++ b/microsite/blog/2021-07-26-software-templates-are-now-in-beta.md @@ -0,0 +1,68 @@ +--- +title: Software Templates is now in Beta +author: Ben Lambert, Spotify +authorURL: https://github.com/benjdlambert +authorImageURL: https://avatars.githubusercontent.com/u/3645856?v=4 +--- + +![Software Templates in Beta](assets/21-07-26/backstage-software-templates-beta.gif) + +**TLDR**; At a high level, Backstage performs three main jobs: [Create, Manage, and Explore](https://backstage.io/blog/2021/05/20/adopting-backstage#three-jobs-create-manage-explore). Throughout the year, we’ve been focussed on improving the stability and scalability of these areas while enhancing the feature sets based upon requests from the community. Software Templates — which enable push-button deployments with all your standards and best practices built-in — is our first feature-complete release candidate to graduate to beta. Read on to learn how this release improves template customization and flexibility, as well as performance. + + + +Let’s rewind a little bit, around a year in fact. Backstage had been open sourced, and we released our first version of Software Templates and our Scaffolder plugin. + +In case you missed it, [Software Templates](https://backstage.io/blog/2020/08/05/announcing-backstage-software-templates) are part of the Create feature set, where you can set up Backstage to give your engineers the ability to spin up a new microservice, website, or any other software component with your organization’s best practices built-in, right from the start. + +Back when we released the first version of these templates, with the `apiVersion` `backstage.io/v1alpha1`, they were a little restrictive in what they could do. The generated repository could only have one template source and it was restricted to only being [cookiecutter](https://github.com/cookiecutter/cookiecutter) templates. And it had to fit into the three stages of templating which was `Prepare`, `Template`, and `Publish`. + +We [opened up for feedback](https://github.com/backstage/backstage/issues/2771) from the community knowing that this wasn’t going to be the final version, but something of a stepping stone to where we want to be in the future. + +And now we come to today… + +## Anddddd… Action(s)! 🎬 + +Following the removal of the previously deprecated `backstage.io/v1alpha1` templates, today we’re officially announcing the beta version of Software Templates, or `backstage.io/v1beta2`. + +With it comes an entirely new architecture to the Software Templates functionality, and a load of new exciting features, which I’ll dig into a little deeper. + +### Template actions + +We’ve totally opened up the ability to compose your own workflows for each Template that you have registered in Software Templates. This means no restrictions on where your template skeleton is kept, and no restrictions on what your workflow should look like. + +Template Actions are customizable JavaScript functions that allow you to extend the capabilities of the Software Templates by adding your own business logic or reusing some of our Built-In actions. + +![New template syntax with actions](assets/21-07-26/example-template-code.png) + +_An example of the new template syntax with steps and parameters_ + +The workflow for Software Templates are now step-based, and each Software Template can have a customizable amount of steps which will in turn call these new actions. + +![New steps being rendered in the frontend](assets/21-07-26/steps-running.gif) + +_An example of a Template with customizable steps_ + +These actions are very heavily inspired by GitHub Actions and GitHub Workflows, and we’ve given back a lot of control to the template authors so they can build the boilerplate and push the code however they want. + +### Custom UI extensions + +A large part of the Software Templates feature is the form that we use to collect user input which is then passed on to these new actions. Sometimes, it makes sense that Template Authors want to provide their own React components which can be rendered in the frontend to maybe do some more complex things like calling internal APIs to provide autocomplete. + +This is now possible with the new `ScaffolderFieldExtensions`. + +![Custom UI extensions](assets/21-07-26/custom-components.gif) + +_An example of `ScaffolderFieldExtension` is the `OwnerPicker` component which can automatically add options and suggest typeahead with data from an async API request_ + +Template Authors can create their own components to provide data to the custom actions in any way you see fit, and even override our own built in ones too like the `RepoUrlPicker`. + +### Horizontal scalability + +The Scaffolder Service and Software Templates are now horizontally scalable, coordinating work between your deployed backend machines. This means now that the number of instances of the `@backstage/plugin-scaffolder-backend` plugin you have, is the same amount of jobs that you can have in parallel. This ensures that your tasks are handled quickly and reliably, while providing great feedback in the process. + +## Try out the new templates + +We’re still on the journey to making the Software Templates a great user experience for both authors and developers — but we’re not done yet, so watch this space for new exciting features to come! + +In the meantime, make sure to visit the documentation for [building templates](https://backstage.io/docs/features/software-templates/adding-templates) or creating your own [custom actions](https://backstage.io/docs/features/software-templates/writing-custom-actions). diff --git a/microsite/blog/assets/21-07-26/backstage-software-templates-beta.gif b/microsite/blog/assets/21-07-26/backstage-software-templates-beta.gif new file mode 100644 index 0000000000..f67113f44c Binary files /dev/null and b/microsite/blog/assets/21-07-26/backstage-software-templates-beta.gif differ diff --git a/microsite/blog/assets/21-07-26/custom-components.gif b/microsite/blog/assets/21-07-26/custom-components.gif new file mode 100644 index 0000000000..d9c78db33f Binary files /dev/null and b/microsite/blog/assets/21-07-26/custom-components.gif differ diff --git a/microsite/blog/assets/21-07-26/example-template-code.png b/microsite/blog/assets/21-07-26/example-template-code.png new file mode 100644 index 0000000000..1205adf657 Binary files /dev/null and b/microsite/blog/assets/21-07-26/example-template-code.png differ diff --git a/microsite/blog/assets/21-07-26/steps-running.gif b/microsite/blog/assets/21-07-26/steps-running.gif new file mode 100644 index 0000000000..d824464bac Binary files /dev/null and b/microsite/blog/assets/21-07-26/steps-running.gif differ diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 8e69b7ff6f..1ea591b1d5 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -44,6 +44,10 @@ class Footer extends React.Component { Contributing + Adopting + + Community Sessions + Subscribe to our newsletter diff --git a/microsite/data/plugins/allure.yaml b/microsite/data/plugins/allure.yaml new file mode 100644 index 0000000000..defa7158c0 --- /dev/null +++ b/microsite/data/plugins/allure.yaml @@ -0,0 +1,9 @@ +--- +title: Allure Reports +author: Deepak Bhardwaj +authorUrl: https://github.com/deepak-bhardwaj-ps +category: Reporting +description: View Allure reports for your components in Backstage. +documentation: https://github.com/backstage/backstage/tree/master/plugins/allure +iconUrl: https://avatars.githubusercontent.com/u/5879127 +npmPackageName: '@backstage/plugin-allure' diff --git a/microsite/data/plugins/aws-cloudformation.yaml b/microsite/data/plugins/aws-cloudformation.yaml new file mode 100644 index 0000000000..7f0feee143 --- /dev/null +++ b/microsite/data/plugins/aws-cloudformation.yaml @@ -0,0 +1,9 @@ +--- +title: AWS CloudFormation +author: Purple Technology +authorUrl: https://blog.purple-technology.com +category: Infrastructure +description: Load Backstage entities from AWS CloudFormation stacks +documentation: https://github.com/purple-technology/backstage-aws-cloudformation-plugin#readme +iconUrl: https://raw.githubusercontent.com/purple-technology/backstage-aws-cloudformation-plugin/master/docs/cloudformation-logo.png +npmPackageName: 'backstage-aws-cloudformation-plugin' diff --git a/microsite/data/plugins/firehydrant.yaml b/microsite/data/plugins/firehydrant.yaml new file mode 100644 index 0000000000..e4dd5e7416 --- /dev/null +++ b/microsite/data/plugins/firehydrant.yaml @@ -0,0 +1,9 @@ +--- +title: FireHydrant +author: FireHydrant +authorUrl: https://firehydrant.io/ +category: Incident Management +description: View service incidents information from FireHydrant, such as active incidents and incident metrics, directly within Backstage. +documentation: https://github.com/backstage/backstage/blob/master/plugins/firehydrant/README.md +iconUrl: https://raw.githubusercontent.com/backstage/backstage/master/plugins/firehydrant/doc/firehydrant_logo.png +npmPackageName: '@backstage/plugin-firehydrant' diff --git a/microsite/data/plugins/gitlab.yaml b/microsite/data/plugins/gitlab.yaml new file mode 100644 index 0000000000..7a5493c7f4 --- /dev/null +++ b/microsite/data/plugins/gitlab.yaml @@ -0,0 +1,12 @@ +--- +title: GitLab +author: Loblaw +authorUrl: https://github.com/loblaw-sre/backstage-plugin-gitlab +category: CI/CD +description: View GitLab pipelines, merge requests, languages and contributors. +documentation: https://github.com/loblaw-sre/backstage-plugin-gitlab +iconUrl: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png +npmPackageName: '@loblaw/backstage-plugin-gitlab' +tags: + - ci + - cd diff --git a/microsite/data/plugins/home.yaml b/microsite/data/plugins/home.yaml new file mode 100644 index 0000000000..e37b8c5e03 --- /dev/null +++ b/microsite/data/plugins/home.yaml @@ -0,0 +1,9 @@ +--- +title: Home +author: Spotify +authorUrl: https://github.com/spotify +category: Discovery +description: This plugin provides a composable home page, and ability to create home page components +documentation: https://github.com/backstage/backstage/blob/master/plugins/home/README.md +iconUrl: img/home.png +npmPackageName: '@backstage/plugin-home' diff --git a/microsite/data/plugins/xcmetrics.yaml b/microsite/data/plugins/xcmetrics.yaml new file mode 100644 index 0000000000..f2f4f7c013 --- /dev/null +++ b/microsite/data/plugins/xcmetrics.yaml @@ -0,0 +1,9 @@ +--- +title: XCMetrics +author: Spotify +authorUrl: https://github.com/spotify +category: Monitoring +description: Discover valuable insights hiding inside Xcode’s build logs. +documentation: https://xcmetrics.io/ +iconUrl: img/xcmetrics-icon.png +npmPackageName: '@backstage/plugin-xcmetrics' diff --git a/microsite/package.json b/microsite/package.json index eee549fb70..b3048612a7 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -16,10 +16,10 @@ "lock:check": "yarn-lock-check" }, "devDependencies": { - "@spotify/prettier-config": "^10.0.0", + "@spotify/prettier-config": "^11.0.0", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", - "prettier": "^2.3.2", + "prettier": "^2.4.0", "yarn-lock-check": "^1.0.5" }, "prettier": "@spotify/prettier-config" diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js index d7325bb234..ec80bbc01f 100644 --- a/microsite/pages/en/index.js +++ b/microsite/pages/en/index.js @@ -212,9 +212,9 @@ class Index extends React.Component { Backstage Software Templates{' '} - (alpha) + (beta) Standards can set you free @@ -292,7 +292,7 @@ class Index extends React.Component { Build your own software templates Contribute diff --git a/microsite/scripts/verify-sidebars.js b/microsite/scripts/verify-sidebars.js index 8d643b15a2..301a1cb4c0 100755 --- a/microsite/scripts/verify-sidebars.js +++ b/microsite/scripts/verify-sidebars.js @@ -28,15 +28,18 @@ try { } const errors = []; -const ids = Object.keys(metadata); -for (let id of ids) { + +// reference/index is generated, so make sure this goes through even if it's not there +const knownIds = new Set([...Object.keys(metadata), 'reference/index']); + +for (const id in metadata) { const { next, previous } = metadata[id]; - if (next && !ids.includes(next)) { + if (next && !knownIds.has(next)) { errors.push(`Next ${next} does not exist in ${id}.`); } - if (previous && !ids.includes(previous)) { + if (previous && !knownIds.has(previous)) { errors.push(`Previous ${previous} does not exist in ${id}.`); } } diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 5999524073..d0d77e9b59 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -36,6 +36,7 @@ "label": "Software Catalog", "ids": [ "features/software-catalog/software-catalog-overview", + "features/software-catalog/life-of-an-entity", "features/software-catalog/configuration", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", @@ -80,7 +81,8 @@ "features/search/search-overview", "features/search/getting-started", "features/search/concepts", - "features/search/architecture" + "features/search/architecture", + "features/search/search-engines" ] }, { @@ -103,6 +105,11 @@ ], "Integrations": [ "integrations/index", + { + "type": "subcategory", + "label": "AWS S3", + "ids": ["integrations/aws-s3/locations"] + }, { "type": "subcategory", "label": "Azure", @@ -227,21 +234,16 @@ "dls/contributing-to-storybook", "dls/figma" ], - "API references": [ + "API Reference": [ { "type": "subcategory", - "label": "TypeScript API", - "ids": [ - "api/utility-apis", - "reference/utility-apis/README", - "reference/createPlugin", - "reference/createPlugin-feature-flags" - ] + "label": "Guides", + "ids": ["api/utility-apis"] }, { "type": "subcategory", - "label": "Backend APIs", - "ids": ["api/backend"] + "label": "API Reference", + "ids": ["reference/index"] } ], "Tutorials": [ diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index 2c8b46ff00..393a5af118 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -288,6 +288,16 @@ td { .tocToggler { display: none; } + + .docsSliderActive .toc section .navGroups { + padding-top: 0; + padding-bottom: 0; + } + + .docsSliderActive.docsNavContainer { + background-color: #121212; + padding-bottom: 0; + } } /* content */ diff --git a/microsite/static/img/backstage-k8s.svg b/microsite/static/img/backstage-k8s.svg index 2796a396e7..28b6ef8f92 100644 --- a/microsite/static/img/backstage-k8s.svg +++ b/microsite/static/img/backstage-k8s.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/microsite/static/img/backstage-software-catalog.svg b/microsite/static/img/backstage-software-catalog.svg index 101439497b..5cad498d35 100644 --- a/microsite/static/img/backstage-software-catalog.svg +++ b/microsite/static/img/backstage-software-catalog.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/microsite/static/img/backstage-software-templates.svg b/microsite/static/img/backstage-software-templates.svg index 3984cd2870..e64428de82 100644 --- a/microsite/static/img/backstage-software-templates.svg +++ b/microsite/static/img/backstage-software-templates.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/microsite/static/img/backstage-techdocs.svg b/microsite/static/img/backstage-techdocs.svg index aa9362b1bc..643cde325c 100644 --- a/microsite/static/img/backstage-techdocs.svg +++ b/microsite/static/img/backstage-techdocs.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/microsite/static/img/home.png b/microsite/static/img/home.png new file mode 100644 index 0000000000..e217581586 Binary files /dev/null and b/microsite/static/img/home.png differ diff --git a/microsite/static/img/rails-icon.png b/microsite/static/img/rails-icon.png index f7b8b69bd9..04f63766fe 100644 Binary files a/microsite/static/img/rails-icon.png and b/microsite/static/img/rails-icon.png differ diff --git a/microsite/static/img/xcmetrics-icon.png b/microsite/static/img/xcmetrics-icon.png new file mode 100644 index 0000000000..09f3ea1e68 Binary files /dev/null and b/microsite/static/img/xcmetrics-icon.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 1da9288808..2973f364b0 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -909,10 +909,10 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@spotify/prettier-config@^10.0.0": - version "10.0.0" - resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-10.0.0.tgz#fa076d98d2e7e6c53dd3d86a696307a7010bd056" - integrity sha512-VYOdo8P7lIScAkl02nB9KpUAuOYMManryBIBuKJkAw5D3aVtLobfmdIKvdV6MqEmGMEQPbn7w/UpnjJYhUH+IA== +"@spotify/prettier-config@^11.0.0": + version "11.0.0" + resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-11.0.0.tgz#d91e0546a8c1c0f7299e2edc7e44306e9be210f6" + integrity sha512-dOI13j1uHMZkRxhZuge/ugOE7Aqcg7Nxki932lDZuXyY4G8CGxkc/66PeQ8pR4PCzThHORXo7Ptvau6bh101lQ== "@types/cheerio@^0.22.8": version "0.22.23" @@ -5207,10 +5207,10 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +prettier@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" + integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== prismjs@^1.22.0: version "1.23.0" diff --git a/mkdocs.yml b/mkdocs.yml index 4d984e63e3..3ffd4aa97d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' + - The Life of an Entity: 'features/software-catalog/life-of-an-entity.md' - Catalog Configuration: 'features/software-catalog/configuration.md' - System Model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' @@ -64,6 +65,7 @@ nav: - Getting Started: 'features/search/getting-started.md' - Concepts: 'features/search/concepts.md' - Search Architecture: 'features/search/architecture.md' + - Search Engines: 'features/search/search-engines.md' - TechDocs: - Overview: 'features/techdocs/README.md' - Getting Started: 'features/techdocs/getting-started.md' @@ -78,6 +80,8 @@ nav: - FAQ: 'features/techdocs/FAQ.md' - Integrations: - Overview: 'integrations/index.md' + - AWS S3: + - Locations: 'integrations/aws-s3/locations.md' - Azure: - Locations: 'integrations/azure/locations.md' - Org Data: 'integrations/azure/org.md' @@ -152,14 +156,9 @@ nav: - Design: 'dls/design.md' - Contributing to Storybook: 'dls/contributing-to-storybook.md' - Figma: 'dls/figma.md' - - API references: - - TypeScript API: + - API Reference: + - Guides: - Utility APIs: 'api/utility-apis.md' - - reference/utility-apis/README: 'reference/utility-apis/README.md' - - createPlugin: 'reference/createPlugin.md' - - createPlugin -feature flags: 'reference/createPlugin-feature-flags.md' - - Backend APIs: - - Backend: 'api/backend.md' - Tutorials: - Future developer journey: 'tutorials/journey.md' - Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md' diff --git a/package.json b/package.json index 93517d3374..d362338370 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "lint:docs": "node ./scripts/check-docs-quality", "lint:all": "lerna run lint --", "lint:type-deps": "node scripts/check-type-dependencies.js", - "docgen": "lerna run docgen", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin", @@ -38,9 +37,14 @@ "packages": [ "packages/*", "plugins/*" + ], + "nohoist": [ + "**/@storybook/**", + "**/@storybook" ] }, "resolutions": { + "**/@graphql-codegen/cli/**/ws": "^7.4.6", "**/@roadiehq/**/@backstage/core": "*", "**/@roadiehq/**/@backstage/plugin-catalog": "*", "**/@roadiehq/**/@backstage/catalog-model": "*", @@ -49,11 +53,13 @@ }, "version": "1.0.0", "dependencies": { - "@microsoft/api-documenter": "^7.13.30", - "@microsoft/api-extractor": "^7.18.1", - "@microsoft/api-extractor-model": "^7.13.3" + "@microsoft/api-documenter": "^7.13.47", + "@microsoft/api-extractor": "^7.18.7", + "@microsoft/api-extractor-model": "^7.13.5", + "@microsoft/tsdoc": "^0.13.2" }, "devDependencies": { + "@types/webpack": "^5.28.0", "@changesets/cli": "^2.14.0", "@octokit/openapi-types": "^2.2.0", "@spotify/prettier-config": "^10.0.0", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index cc67b78f44..9b6dc1956a 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,235 @@ # example-app +## 0.2.45 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-todo@0.1.9 + - @backstage/core-components@0.4.1 + - @backstage/catalog-model@0.9.2 + - @backstage/plugin-home@0.3.0 + - @backstage/plugin-scaffolder@0.11.3 + - @backstage/plugin-org@0.3.22 + - @backstage/core-app-api@0.1.12 + - @backstage/plugin-search@0.4.10 + - @backstage/core-plugin-api@0.1.7 + - @backstage/cli@0.7.11 + +## 0.2.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@0.11.2 + - @backstage/cli@0.7.10 + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/plugin-techdocs@0.11.1 + - @backstage/core-components@0.4.0 + - @backstage/plugin-catalog-import@0.5.20 + - @backstage/plugin-shortcuts@0.1.7 + - @backstage/plugin-org@0.3.21 + - @backstage/plugin-catalog@0.6.14 + - @backstage/search-common@0.2.0 + - @backstage/plugin-search@0.4.9 + - @backstage/catalog-model@0.9.1 + - @backstage/core-app-api@0.1.11 + - @backstage/integration-react@0.1.8 + - @backstage/plugin-api-docs@0.6.7 + - @backstage/plugin-badges@0.2.8 + - @backstage/plugin-circleci@0.2.22 + - @backstage/plugin-cloudbuild@0.2.22 + - @backstage/plugin-code-coverage@0.1.10 + - @backstage/plugin-cost-insights@0.11.5 + - @backstage/plugin-explore@0.3.15 + - @backstage/plugin-gcp-projects@0.3.3 + - @backstage/plugin-github-actions@0.4.17 + - @backstage/plugin-graphiql@0.2.15 + - @backstage/plugin-home@0.2.1 + - @backstage/plugin-jenkins@0.5.5 + - @backstage/plugin-kafka@0.2.14 + - @backstage/plugin-kubernetes@0.4.12 + - @backstage/plugin-lighthouse@0.2.24 + - @backstage/plugin-newrelic@0.3.3 + - @backstage/plugin-pagerduty@0.3.12 + - @backstage/plugin-rollbar@0.3.13 + - @backstage/plugin-sentry@0.3.20 + - @backstage/plugin-tech-radar@0.4.6 + - @backstage/plugin-todo@0.1.8 + - @backstage/plugin-user-settings@0.3.4 + +## 0.2.43 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-explore@0.3.14 + - @backstage/core-app-api@0.1.10 + - @backstage/plugin-catalog-import@0.5.19 + - @backstage/plugin-catalog@0.6.13 + - @backstage/plugin-org@0.3.20 + - @backstage/plugin-catalog-react@0.4.4 + - @backstage/core-components@0.3.3 + - @backstage/plugin-techdocs@0.11.0 + - @backstage/plugin-search@0.4.8 + +## 0.2.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@0.11.1 + - @backstage/plugin-home@0.2.0 + - @backstage/cli@0.7.9 + - @backstage/core-app-api@0.1.9 + - @backstage/plugin-catalog-import@0.5.18 + - @backstage/plugin-catalog@0.6.12 + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/plugin-sentry@0.3.19 + - @backstage/plugin-jenkins@0.5.4 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + +## 0.2.41 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cost-insights@0.11.4 + - @backstage/plugin-github-actions@0.4.16 + - @backstage/plugin-jenkins@0.5.3 + - @backstage/plugin-kubernetes@0.4.11 + - @backstage/plugin-pagerduty@0.3.11 + - @backstage/plugin-scaffolder@0.11.0 + - @backstage/plugin-sentry@0.3.18 + - @backstage/plugin-techdocs@0.10.4 + - @backstage/core-app-api@0.1.8 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-api-docs@0.6.6 + - @backstage/plugin-catalog@0.6.11 + - @backstage/plugin-catalog-import@0.5.17 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/plugin-explore@0.3.13 + - @backstage/plugin-lighthouse@0.2.23 + - @backstage/plugin-rollbar@0.3.12 + - @backstage/plugin-search@0.4.7 + - @backstage/plugin-tech-radar@0.4.5 + - @backstage/cli@0.7.8 + - @backstage/integration-react@0.1.7 + +## 0.2.40 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.7.7 + - @backstage/plugin-catalog@0.6.10 + - @backstage/core-components@0.3.0 + - @backstage/core-plugin-api@0.1.5 + - @backstage/plugin-api-docs@0.6.5 + - @backstage/plugin-scaffolder@0.10.3 + - @backstage/plugin-search@0.4.6 + - @backstage/plugin-cost-insights@0.11.3 + - @backstage/plugin-kubernetes@0.4.10 + - @backstage/plugin-lighthouse@0.2.22 + - @backstage/plugin-pagerduty@0.3.10 + - @backstage/plugin-techdocs@0.10.3 + - @backstage/search-common@0.1.3 + - @backstage/core-app-api@0.1.7 + - @backstage/integration-react@0.1.6 + - @backstage/plugin-badges@0.2.7 + - @backstage/plugin-catalog-import@0.5.16 + - @backstage/plugin-catalog-react@0.4.1 + - @backstage/plugin-circleci@0.2.21 + - @backstage/plugin-cloudbuild@0.2.21 + - @backstage/plugin-code-coverage@0.1.9 + - @backstage/plugin-explore@0.3.12 + - @backstage/plugin-gcp-projects@0.3.2 + - @backstage/plugin-github-actions@0.4.15 + - @backstage/plugin-graphiql@0.2.14 + - @backstage/plugin-jenkins@0.5.2 + - @backstage/plugin-kafka@0.2.13 + - @backstage/plugin-newrelic@0.3.2 + - @backstage/plugin-org@0.3.19 + - @backstage/plugin-rollbar@0.3.11 + - @backstage/plugin-sentry@0.3.17 + - @backstage/plugin-shortcuts@0.1.6 + - @backstage/plugin-tech-radar@0.4.4 + - @backstage/plugin-todo@0.1.7 + - @backstage/plugin-user-settings@0.3.3 + +## 0.2.39 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@0.10.2 + - @backstage/core-components@0.2.0 + - @backstage/plugin-catalog@0.6.9 + - @backstage/plugin-catalog-react@0.4.0 + - @backstage/cli@0.7.6 + - @backstage/core-app-api@0.1.6 + - @backstage/core-plugin-api@0.1.4 + - @backstage/integration-react@0.1.5 + - @backstage/theme@0.2.9 + - @backstage/plugin-api-docs@0.6.4 + - @backstage/plugin-badges@0.2.6 + - @backstage/plugin-catalog-import@0.5.15 + - @backstage/plugin-circleci@0.2.20 + - @backstage/plugin-cloudbuild@0.2.20 + - @backstage/plugin-code-coverage@0.1.8 + - @backstage/plugin-cost-insights@0.11.2 + - @backstage/plugin-explore@0.3.11 + - @backstage/plugin-gcp-projects@0.3.1 + - @backstage/plugin-github-actions@0.4.14 + - @backstage/plugin-graphiql@0.2.13 + - @backstage/plugin-jenkins@0.5.1 + - @backstage/plugin-kafka@0.2.12 + - @backstage/plugin-kubernetes@0.4.9 + - @backstage/plugin-lighthouse@0.2.21 + - @backstage/plugin-newrelic@0.3.1 + - @backstage/plugin-org@0.3.18 + - @backstage/plugin-pagerduty@0.3.9 + - @backstage/plugin-rollbar@0.3.10 + - @backstage/plugin-search@0.4.5 + - @backstage/plugin-sentry@0.3.16 + - @backstage/plugin-shortcuts@0.1.5 + - @backstage/plugin-tech-radar@0.4.3 + - @backstage/plugin-techdocs@0.10.2 + - @backstage/plugin-todo@0.1.6 + - @backstage/plugin-user-settings@0.3.2 + +## 0.2.37 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@0.6.7 + - @backstage/plugin-catalog-react@0.3.0 + - @backstage/plugin-scaffolder@0.10.1 + - @backstage/plugin-techdocs@0.10.0 + - @backstage/plugin-jenkins@0.5.0 + - @backstage/plugin-user-settings@0.3.0 + - @backstage/plugin-search@0.4.3 + - @backstage/cli@0.7.4 + - @backstage/plugin-api-docs@0.6.2 + - @backstage/plugin-badges@0.2.5 + - @backstage/plugin-catalog-import@0.5.13 + - @backstage/plugin-circleci@0.2.19 + - @backstage/plugin-cloudbuild@0.2.19 + - @backstage/plugin-code-coverage@0.1.7 + - @backstage/plugin-explore@0.3.10 + - @backstage/plugin-github-actions@0.4.13 + - @backstage/plugin-kafka@0.2.11 + - @backstage/plugin-kubernetes@0.4.8 + - @backstage/plugin-lighthouse@0.2.20 + - @backstage/plugin-org@0.3.17 + - @backstage/plugin-pagerduty@0.3.8 + - @backstage/plugin-rollbar@0.3.9 + - @backstage/plugin-sentry@0.3.15 + - @backstage/plugin-todo@0.1.5 + ## 0.2.36 ### Patch Changes diff --git a/packages/app/cypress/integration/components/search/SearchPage.js b/packages/app/cypress/integration/components/search/SearchPage.js index 4db7acf4fd..6a18d16198 100644 --- a/packages/app/cypress/integration/components/search/SearchPage.js +++ b/packages/app/cypress/integration/components/search/SearchPage.js @@ -34,7 +34,7 @@ describe('SearchPage', () => { cy.visit('/search-next', { onBeforeLoad(win) { cy.stub(win, 'fetch') - .withArgs(`${API_ENDPOINT}?term=&pageCursor=`) + .withArgs(`${API_ENDPOINT}?term=`) .resolves({ ok: true, json: () => ({ results }), @@ -56,7 +56,7 @@ describe('SearchPage', () => { onBeforeLoad(win) { cy.stub(win, 'fetch') .withArgs( - `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental&pageCursor=`, + `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental`, ) .resolves({ ok: true, @@ -102,7 +102,7 @@ describe('SearchPage', () => { cy.visit('/search-next?query=backstage', { onBeforeLoad(win) { cy.stub(win, 'fetch') - .withArgs(`${API_ENDPOINT}?term=backstage&pageCursor=`) + .withArgs(`${API_ENDPOINT}?term=backstage`) .resolves({ ok: true, json: () => ({ results: [] }), diff --git a/packages/app/package.json b/packages/app/package.json index b26e94fc26..db7a99e632 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,52 +1,55 @@ { "name": "example-app", - "version": "0.2.36", + "version": "0.2.45", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/cli": "^0.7.2", - "@backstage/core-app-api": "^0.1.3", - "@backstage/core-components": "^0.1.5", - "@backstage/core-plugin-api": "^0.1.3", - "@backstage/integration-react": "^0.1.4", - "@backstage/plugin-api-docs": "^0.6.1", - "@backstage/plugin-badges": "^0.2.4", - "@backstage/plugin-catalog": "^0.6.6", - "@backstage/plugin-catalog-import": "^0.5.12", - "@backstage/plugin-catalog-react": "^0.2.6", - "@backstage/plugin-circleci": "^0.2.18", - "@backstage/plugin-cloudbuild": "^0.2.18", - "@backstage/plugin-code-coverage": "^0.1.6", - "@backstage/plugin-cost-insights": "^0.11.0", - "@backstage/plugin-explore": "^0.3.9", - "@backstage/plugin-gcp-projects": "^0.3.0", - "@backstage/plugin-github-actions": "^0.4.12", - "@backstage/plugin-graphiql": "^0.2.12", - "@backstage/plugin-jenkins": "^0.4.7", - "@backstage/plugin-kafka": "^0.2.10", - "@backstage/plugin-kubernetes": "^0.4.7", - "@backstage/plugin-lighthouse": "^0.2.19", - "@backstage/plugin-newrelic": "^0.3.0", - "@backstage/plugin-org": "^0.3.16", - "@backstage/plugin-pagerduty": "0.3.7", - "@backstage/plugin-rollbar": "^0.3.8", - "@backstage/plugin-scaffolder": "^0.10.0", - "@backstage/plugin-search": "^0.4.2", - "@backstage/plugin-sentry": "^0.3.14", - "@backstage/plugin-shortcuts": "^0.1.4", - "@backstage/plugin-tech-radar": "^0.4.1", - "@backstage/plugin-techdocs": "^0.9.9", - "@backstage/plugin-todo": "^0.1.4", - "@backstage/plugin-user-settings": "^0.2.12", - "@backstage/theme": "^0.2.8", - "@material-ui/core": "^4.11.0", + "@backstage/catalog-model": "^0.9.2", + "@backstage/cli": "^0.7.11", + "@backstage/core-app-api": "^0.1.12", + "@backstage/core-components": "^0.4.1", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/integration-react": "^0.1.8", + "@backstage/plugin-api-docs": "^0.6.7", + "@backstage/plugin-badges": "^0.2.8", + "@backstage/plugin-catalog": "^0.6.14", + "@backstage/plugin-catalog-import": "^0.5.20", + "@backstage/plugin-catalog-react": "^0.4.5", + "@backstage/plugin-circleci": "^0.2.22", + "@backstage/plugin-cloudbuild": "^0.2.22", + "@backstage/plugin-code-coverage": "^0.1.10", + "@backstage/plugin-cost-insights": "^0.11.5", + "@backstage/plugin-explore": "^0.3.15", + "@backstage/plugin-gcp-projects": "^0.3.3", + "@backstage/plugin-github-actions": "^0.4.17", + "@backstage/plugin-graphiql": "^0.2.15", + "@backstage/plugin-home": "^0.3.0", + "@backstage/plugin-jenkins": "^0.5.5", + "@backstage/plugin-kafka": "^0.2.14", + "@backstage/plugin-kubernetes": "^0.4.12", + "@backstage/plugin-lighthouse": "^0.2.24", + "@backstage/plugin-newrelic": "^0.3.3", + "@backstage/plugin-org": "^0.3.22", + "@backstage/plugin-pagerduty": "0.3.12", + "@backstage/plugin-rollbar": "^0.3.13", + "@backstage/plugin-scaffolder": "^0.11.3", + "@backstage/plugin-search": "^0.4.10", + "@backstage/plugin-sentry": "^0.3.20", + "@backstage/plugin-shortcuts": "^0.1.7", + "@backstage/plugin-tech-radar": "^0.4.6", + "@backstage/plugin-techdocs": "^0.11.1", + "@backstage/plugin-todo": "^0.1.9", + "@backstage/plugin-user-settings": "^0.3.4", + "@backstage/search-common": "^0.2.0", + "@backstage/theme": "^0.2.10", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.5.3", - "@roadiehq/backstage-plugin-buildkite": "^1.0.4", - "@roadiehq/backstage-plugin-github-insights": "^1.1.15", - "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.8", - "@roadiehq/backstage-plugin-travis-ci": "^1.0.4", + "@roadiehq/backstage-plugin-buildkite": "^1.0.8", + "@roadiehq/backstage-plugin-github-insights": "^1.1.23", + "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13", + "@roadiehq/backstage-plugin-travis-ci": "^1.0.11", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^16.12.0", @@ -58,7 +61,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.17", + "@rjsf/core": "^3.0.0", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", @@ -66,7 +70,7 @@ "@types/jest": "^26.0.7", "@types/jquery": "^3.3.34", "@types/node": "^14.14.32", - "@types/react-dom": "^16.9.8", + "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", "cross-env": "^7.0.0", "cypress": "^7.3.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index de62971f96..81dbdf4cac 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,6 +20,7 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; +import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { CatalogEntityPage, @@ -41,10 +42,19 @@ import { GcpProjectsPage } from '@backstage/plugin-gcp-projects'; import { GraphiQLPage } from '@backstage/plugin-graphiql'; import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; -import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { + ScaffolderPage, + scaffolderPlugin, + ScaffolderFieldExtensions, +} from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; -import { TechdocsPage } from '@backstage/plugin-techdocs'; +import { + DefaultTechDocsHome, + TechDocsIndexPage, + techdocsPlugin, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; import AlarmIcon from '@material-ui/icons/Alarm'; import React from 'react'; @@ -54,6 +64,9 @@ import { apis } from './apis'; import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; +import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions'; +import { HomePage } from './components/home/HomePage'; + import { providers } from './identityProviders'; import * as plugins from './plugins'; @@ -80,6 +93,7 @@ const app = createApp({ bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, }); bind(apiDocsPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, @@ -98,7 +112,11 @@ const AppRouter = app.getRouter(); const routes = ( - + + {/* TODO(rubenl): Move this to / once its more mature and components exist */} + }> + + } /> } /> - } /> - } /> + }> + + + } + /> + }> + + + + } /> } /> } /> - } /> } /> } /> diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 48141e4f53..7a666a020a 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -82,7 +82,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( {/* Global nav, not org-specific */} - + diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 16107a6cc0..4a392e0f05 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -40,6 +40,8 @@ import { isKind, EntityHasResourcesCard, EntityOrphanWarning, + EntityProcessingErrorsPanel, + hasCatalogProcessingErrors, isOrphan, } from '@backstage/plugin-catalog'; import { @@ -135,6 +137,13 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => { ); }; +/** + * NOTE: This page is designed to work on small screens such as mobile devices. + * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`, + * since this does not default. If no breakpoints are used, the items will equitably share the asvailable space. + * https://material-ui.com/components/grid/#basic-grid. + */ + export const cicdContent = ( @@ -202,6 +211,26 @@ const cicdCard = ( ); +const entityWarningContent = ( + <> + + + + + + + + + + + + + + + + +); + const errorsContent = ( @@ -216,14 +245,7 @@ const errorsContent = ( const overviewContent = ( - - - - - - - - + {entityWarningContent} @@ -292,10 +314,10 @@ const serviceEntityPage = ( - + - + @@ -303,10 +325,10 @@ const serviceEntityPage = ( - + - + @@ -431,15 +453,18 @@ const apiPage = ( - + {entityWarningContent} + - - - - - - + + + + + + + + @@ -459,6 +484,7 @@ const userPage = ( + {entityWarningContent} @@ -474,6 +500,7 @@ const groupPage = ( + {entityWarningContent} @@ -492,6 +519,7 @@ const systemPage = ( + {entityWarningContent} @@ -516,6 +544,7 @@ const domainPage = ( + {entityWarningContent} diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx new file mode 100644 index 0000000000..ebb486790d --- /dev/null +++ b/packages/app/src/components/home/HomePage.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import Grid from '@material-ui/core/Grid'; +import { + HomePageRandomJoke, + ComponentAccordion, + ComponentTabs, + ComponentTab, +} from '@backstage/plugin-home'; +import { + Content, + Header, + Page, + HomepageTimer, +} from '@backstage/core-components'; +import { HomePageSearchBar } from '@backstage/plugin-search'; + +export const HomePage = () => ( + +
+ +
+ + + + + + + + + + + + + + + ( + + ), + }, + { + label: 'Any', + Component: () => ( + + ), + }, + ]} + /> + + + +
+); diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx new file mode 100644 index 0000000000..5ef4f617a7 --- /dev/null +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 type { FieldValidation } from '@rjsf/core'; +import { + createScaffolderFieldExtension, + TextValuePicker, + scaffolderPlugin, +} from '@backstage/plugin-scaffolder'; + +export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + name: 'LowerCaseValuePicker', + component: TextValuePicker, + validation: (value: string, validation: FieldValidation) => { + if (value.toLowerCase() !== value) { + validation.addError('Only lowercase values are allowed.'); + } + }, + }), +); diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 35c4a4ac1c..54f59d1a6e 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -14,35 +14,36 @@ * limitations under the License. */ -import React from 'react'; -import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; - +import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; import { + DefaultResultListItem, SearchBar, SearchFilter, SearchResult, - DefaultResultListItem, + SearchResultPager, + SearchType, } from '@backstage/plugin-search'; -import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; +import { DocsResultListItem } from '@backstage/plugin-techdocs'; +import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; +import React from 'react'; const useStyles = makeStyles((theme: Theme) => ({ bar: { padding: theme.spacing(1, 0), }, - filters: { - padding: theme.spacing(2), - }, filter: { '& + &': { marginTop: theme.spacing(2.5), }, }, + filters: { + padding: theme.spacing(2), + }, })); const SearchPage = () => { const classes = useStyles(); - return (
} /> @@ -55,6 +56,11 @@ const SearchPage = () => { + { result={document} /> ); + case 'techdocs': + return ( + + ); default: return ( { )} + diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 93016bd13c..7ffd6e52dc 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,107 @@ # @backstage/backend-common +## 0.9.2 + +### Patch Changes + +- 9e5ed27ec: Properly export all used types. +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config-loader@0.6.8 + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + +## 0.9.1 + +### Patch Changes + +- 714a2a918: Export type that are needed to implement a new `UrlReader` +- Updated dependencies + - @backstage/integration@0.6.3 + +## 0.9.0 + +### Minor Changes + +- a365f1faf: The `ZipArchiveResponse` class now accepts an optional `stripFirstDirectory` parameter. Note that its default value is `false`, which leads to a breaking change in behaviour to previous versions of the class. If you use this class explicitly and want to retain the old behaviour, add a `true` parameter value to its constructor. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.2 + - @backstage/config@0.1.8 + +## 0.8.10 + +### Patch Changes + +- 8543d9890: Add an optional `info` parameter to the `readTree` filter option with a `size` property. +- 4d909268c: Read responses in `UrlReader#read()` as array buffer instead of as text to allow reading non-text locations such as images. +- 9b4604b38: Add support for watching configuration by implementing the `subscribe` method in the configuration returned by `loadBackendConfig`. +- b8cb12009: Add AWS S3 URL Reader +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/config-loader@0.6.7 + - @backstage/integration@0.6.1 + +## 0.8.9 + +### Patch Changes + +- f7ce7c565: Use a more informative error message when URL reading isn't allowed due to no reader matching the target URL. +- ce1958021: Pass on credentials to the integrations package, so that it can properly pick the API route when using GitHub apps based auth +- Updated dependencies + - @backstage/integration@0.6.0 + +## 0.8.8 + +### Patch Changes + +- 6aa7c3db7: bump node-tar version to the latest +- Updated dependencies + - @backstage/config@0.1.6 + - @backstage/integration@0.5.9 + - @backstage/config-loader@0.6.6 + +## 0.8.7 + +### Patch Changes + +- f25357273: Implement the etag functionality in the `readUrl` method of `FetchUrlReader`. +- bdd6ab5f1: It's possible to customize the request logging handler when building the service. For example in your `backend` + + ``` + const service = createServiceBuilder(module) + .loadConfig(config) + .setRequestLoggingHandler((logger?: Logger): RequestHandler => { + const actualLogger = (logger || getRootLogger()).child({ + type: 'incomingRequest', + }); + return expressWinston.logger({ ... + ``` + +## 0.8.6 + +### Patch Changes + +- 5f6f2fd96: Support a `ensureExists` config option to skip ensuring a configured database exists. This allows deployment scenarios where + limited permissions are given for provisioned databases without privileges to create new databases. If set to `false`, the + database connection will not be validated prior to use which means the backend will not attempt to create the database if it + doesn't exist. You can configure this in your app-config.yaml: + + ```yaml + backend: + database: + ensureExists: false + ``` + + This defaults to `true` if unspecified. You can also configure this per plugin connection and will override the base option. + +- ad93bb035: Document the default behavior of `statusCheck` option in `createStatusCheckRouter`. +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. +- Updated dependencies + - @backstage/config-loader@0.6.5 + ## 0.8.5 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 94f1147b5a..eb1f9ad31f 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -9,7 +9,6 @@ import { AzureIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; -import { ConfigReader } from '@backstage/config'; import cors from 'cors'; import Docker from 'dockerode'; import { ErrorRequestHandler } from 'express'; @@ -17,7 +16,6 @@ import express from 'express'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; -import * as http from 'http'; import { isChildPath } from '@backstage/cli-common'; import { JsonValue } from '@backstage/config'; import { Knex } from 'knex'; @@ -80,9 +78,23 @@ export class BitbucketUrlReader implements UrlReader { export interface CacheClient { delete(key: string): Promise; get(key: string): Promise; - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + set( + key: string, + value: JsonValue, + options?: CacheClientSetOptions, + ): Promise; } +// @public (undocumented) +export type CacheClientOptions = { + defaultTtl?: number; +}; + +// @public (undocumented) +export type CacheClientSetOptions = { + ttl?: number; +}; + // @public export class CacheManager { forPlugin(pluginId: string): PluginCacheManager; @@ -92,6 +104,12 @@ export class CacheManager { ): CacheManager; } +// @public (undocumented) +export type CacheManagerOptions = { + logger?: Logger_2; + onError?: (err: Error) => void; +}; + // @public (undocumented) export const coloredFormat: winston.Logform.Format; @@ -117,12 +135,14 @@ export function createRootLogger( ): winston.Logger; // @public -export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; +export function createServiceBuilder(_module: NodeModule): ServiceBuilder; // @public (undocumented) -export function createStatusCheckRouter( - options: StatusCheckRouterOptions, -): Promise; +export function createStatusCheckRouter(options: { + logger: Logger_2; + path?: string; + statusCheck?: StatusCheck; +}): Promise; // @public (undocumented) export class DatabaseManager { @@ -142,6 +162,7 @@ export class DockerContainerRunner implements ContainerRunner { mountDirs, workingDir, envVars, + pullImage, }: RunContainerOptions): Promise; } @@ -321,14 +342,17 @@ export class GitlabUrlReader implements UrlReader { export { isChildPath }; // @public -export function loadBackendConfig(options: Options): Promise; +export function loadBackendConfig(options: { + logger: Logger_2; + argv: string[]; +}): Promise; // @public export function notFoundHandler(): RequestHandler; // @public export type PluginCacheManager = { - getClient: (options?: ClientOptions) => CacheClient; + getClient: (options?: CacheClientOptions) => CacheClient; }; // @public @@ -342,6 +366,24 @@ export type PluginEndpointDiscovery = { getExternalBaseUrl(pluginId: string): Promise; }; +// @public +export type ReaderFactory = (options: { + config: Config; + logger: Logger_2; + treeResponseFactory: ReadTreeResponseFactory; +}) => UrlReaderPredicateTuple[]; + +// @public +export type ReadTreeOptions = { + filter?( + path: string, + info?: { + size: number; + }, + ): boolean; + etag?: string; +}; + // @public export type ReadTreeResponse = { files(): Promise; @@ -350,15 +392,61 @@ export type ReadTreeResponse = { etag: string; }; +// @public (undocumented) +export type ReadTreeResponseDirOptions = { + targetDir?: string; +}; + +// @public (undocumented) +export interface ReadTreeResponseFactory { + // (undocumented) + fromTarArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; + // (undocumented) + fromZipArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; +} + +// @public (undocumented) +export type ReadTreeResponseFactoryOptions = { + stream: Readable; + subpath?: string; + etag: string; + filter?: ( + path: string, + info?: { + size: number; + }, + ) => boolean; +}; + // @public export type ReadTreeResponseFile = { path: string; content(): Promise; }; +// @public +export type ReadUrlOptions = { + etag?: string; +}; + +// @public +export type ReadUrlResponse = { + buffer(): Promise; + etag?: string; +}; + // @public export function requestLoggingHandler(logger?: Logger_2): RequestHandler; +// @public (undocumented) +export type RequestLoggingHandlerFactory = ( + logger?: Logger_2, +) => RequestHandler; + // @public export function resolvePackagePath(name: string, ...paths: string[]): string; @@ -374,6 +462,12 @@ export type RunContainerOptions = { mountDirs?: Record; workingDir?: string; envVars?: Record; + pullImage?: boolean; +}; + +// @public +export type SearchOptions = { + etag?: string; }; // @public @@ -390,13 +484,25 @@ export type SearchResponseFile = { // @public (undocumented) export type ServiceBuilder = { - loadConfig(config: ConfigReader): ServiceBuilder; + loadConfig(config: Config): ServiceBuilder; setPort(port: number): ServiceBuilder; setHost(host: string): ServiceBuilder; setLogger(logger: Logger_2): ServiceBuilder; enableCors(options: cors.CorsOptions): ServiceBuilder; - setHttpsSettings(settings: HttpsSettings): ServiceBuilder; + setHttpsSettings(settings: { + certificate: + | { + key: string; + cert: string; + } + | { + hostname: string; + }; + }): ServiceBuilder; addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; + setRequestLoggingHandler( + requestLoggingHandler: RequestLoggingHandlerFactory, + ): ServiceBuilder; start(): Promise; }; @@ -441,12 +547,25 @@ export type UrlReader = { search(url: string, options?: SearchOptions): Promise; }; +// @public (undocumented) +export type UrlReaderPredicateTuple = { + predicate: (url: URL) => boolean; + reader: UrlReader; +}; + // @public export class UrlReaders { - static create({ logger, config, factories }: CreateOptions): UrlReader; - static default({ logger, config, factories }: CreateOptions): UrlReader; + static create({ logger, config, factories }: UrlReadersOptions): UrlReader; + static default({ logger, config, factories }: UrlReadersOptions): UrlReader; } +// @public (undocumented) +export type UrlReadersOptions = { + config: Config; + logger: Logger_2; + factories?: ReaderFactory[]; +}; + // @public export function useHotCleanup( _module: NodeModule, diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 6cf20b8234..6ec2c8b131 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.8.5", + "version": "0.9.2", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,17 +29,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.2", - "@backstage/config": "^0.1.5", - "@backstage/config-loader": "^0.6.4", - "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.8", + "@backstage/cli-common": "^0.1.3", + "@backstage/config": "^0.1.9", + "@backstage/config-loader": "^0.6.8", + "@backstage/errors": "^0.1.2", + "@backstage/integration": "^0.6.3", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "archiver": "^5.0.2", + "aws-sdk": "^2.840.0", "compression": "^1.7.4", "concat-stream": "^2.0.0", "cors": "^2.8.5", @@ -48,7 +49,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", - "git-url-parse": "~11.4.4", + "git-url-parse": "^11.6.0", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "keyv": "^4.0.3", @@ -62,7 +63,7 @@ "raw-body": "^2.4.1", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", - "tar": "^6.0.5", + "tar": "^6.1.2", "unzipper": "^0.10.11", "winston": "^3.2.1", "yn": "^4.0.0" @@ -76,8 +77,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.3", - "@backstage/test-utils": "^0.1.12", + "@backstage/cli": "^0.7.11", + "@backstage/test-utils": "^0.1.17", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", @@ -92,6 +93,7 @@ "@types/tar": "^4.0.3", "@types/unzipper": "^0.10.3", "@types/webpack-env": "^1.15.2", + "aws-sdk-mock": "^5.2.1", "get-port": "^5.1.1", "http-errors": "^1.7.3", "jest": "^26.0.1", diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 860754aae1..79f48a2757 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -22,7 +22,8 @@ type CacheClientArgs = { client: Keyv; }; -type CacheSetOptions = { +/** @public */ +export type CacheClientSetOptions = { /** * Optional TTL in milliseconds. Defaults to the TTL provided when the client * was set up (or no TTL if none are provided). @@ -33,6 +34,8 @@ type CacheSetOptions = { /** * A pre-configured, storage agnostic cache client suitable for use by * Backstage plugins. + * + * @public */ export interface CacheClient { /** @@ -46,7 +49,11 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + set( + key: string, + value: JsonValue, + options?: CacheClientSetOptions, + ): Promise; /** * Removes the given key from the cache store. @@ -73,7 +80,7 @@ export class DefaultCacheClient implements CacheClient { async set( key: string, value: JsonValue, - opts: CacheSetOptions = {}, + opts: CacheClientSetOptions = {}, ): Promise { const k = this.getNormalizedKey(key); await this.client.set(k, value, opts.ttl); diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index b55b63f469..fb73687f54 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -104,7 +104,7 @@ describe('CacheManager', () => { manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl }); const client = DefaultCacheClient as jest.Mock; - const cache = (Keyv as unknown) as jest.Mock; + const cache = Keyv as unknown as jest.Mock; expect(cache).toHaveBeenCalledTimes(2); expect(client).toHaveBeenCalledTimes(2); @@ -124,7 +124,7 @@ describe('CacheManager', () => { const expectedNamespace = 'test-plugin'; manager.forPlugin(expectedNamespace).getClient(); - const cache = (Keyv as unknown) as jest.Mock; + const cache = Keyv as unknown as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; expect(callArgs[0].store).toBeInstanceOf(NoStore); @@ -138,7 +138,7 @@ describe('CacheManager', () => { .forPlugin(expectedNamespace) .getClient({ defaultTtl: expectedTtl }); - const cache = (Keyv as unknown) as jest.Mock; + const cache = Keyv as unknown as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; expect(callArgs[0]).toMatchObject({ @@ -162,7 +162,7 @@ describe('CacheManager', () => { const expectedTtl = 3600; manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); - const cache = (Keyv as unknown) as jest.Mock; + const cache = Keyv as unknown as jest.Mock; const mockCacheCalls = cache.mock.calls.splice(-1); expect(mockCacheCalls[0][0]).toMatchObject({ ttl: expectedTtl, diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 9d1afdaaeb..d3a1504187 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -22,16 +22,14 @@ import { Logger } from 'winston'; import { getRootLogger } from '../logging'; import { DefaultCacheClient, CacheClient } from './CacheClient'; import { NoStore } from './NoStore'; -import { - CacheManagerOptions, - OptionalOnError, - PluginCacheManager, -} from './types'; +import { CacheManagerOptions, PluginCacheManager } from './types'; /** * Implements a Cache Manager which will automatically create new cache clients * for plugins when requested. All requested cache clients are created with the * connection details provided. + * + * @public */ export class CacheManager { /** @@ -47,13 +45,13 @@ export class CacheManager { private readonly logger: Logger; private readonly store: keyof CacheManager['storeFactories']; private readonly connection: string; - private readonly errorHandler: OptionalOnError; + private readonly errorHandler: CacheManagerOptions['onError']; /** * Creates a new CacheManager instance by reading from the `backend` config * section, specifically the `.cache` key. * - * @param config The loaded application configuration. + * @param config - The loaded application configuration. */ static fromConfig( config: Config, @@ -74,7 +72,7 @@ export class CacheManager { store: string, connectionString: string, logger: Logger, - errorHandler: OptionalOnError, + errorHandler: CacheManagerOptions['onError'], ) { if (!this.storeFactories.hasOwnProperty(store)) { throw new Error(`Unknown cache store: ${store}`); @@ -88,7 +86,7 @@ export class CacheManager { /** * Generates a PluginCacheManager for consumption by plugins. * - * @param pluginId The plugin that the cache manager should be created for. Plugin names should be unique. + * @param pluginId - The plugin that the cache manager should be created for. Plugin names should be unique. */ forPlugin(pluginId: string): PluginCacheManager { return { diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index 45eb431ed1..26e6c102d8 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,6 +14,10 @@ * limitations under the License. */ -export type { CacheClient } from './CacheClient'; +export type { CacheClient, CacheClientSetOptions } from './CacheClient'; export { CacheManager } from './CacheManager'; -export type { PluginCacheManager } from './types'; +export type { + PluginCacheManager, + CacheManagerOptions, + CacheClientOptions, +} from './types'; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index ac5bb91b21..70c46770a8 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -17,7 +17,8 @@ import { Logger } from 'winston'; import { CacheClient } from './CacheClient'; -type ClientOptions = { +/** @public */ +export type CacheClientOptions = { /** * An optional default TTL (in milliseconds) to be set when getting a client * instance. If not provided, data will persist indefinitely by default (or @@ -26,8 +27,7 @@ type ClientOptions = { defaultTtl?: number; }; -export type OptionalOnError = ((err: Error) => void) | undefined; - +/** @public */ export type CacheManagerOptions = { /** * An optional logger for use by the PluginCacheManager. @@ -38,11 +38,13 @@ export type CacheManagerOptions = { * An optional handler for connection errors emitted from the underlying data * store. */ - onError?: OptionalOnError; + onError?: (err: Error) => void; }; /** * The PluginCacheManager manages access to cache stores that Plugins get. + * + * @public */ export type PluginCacheManager = { /** @@ -52,5 +54,5 @@ export type PluginCacheManager = { * stores so that plugins are discouraged from cache-level integration * and/or cache key collisions. */ - getClient: (options?: ClientOptions) => CacheClient; + getClient: (options?: CacheClientOptions) => CacheClient; }; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 1316642ae1..96907870cb 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,32 +18,142 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader } from '@backstage/config'; +import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; -type Options = { +class ObservableConfigProxy implements Config { + private config: Config = new ConfigReader({}); + + private readonly subscribers: (() => void)[] = []; + + constructor(private readonly logger: Logger) {} + + setConfig(config: Config) { + this.config = config; + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + this.logger.error(`Config subscriber threw error, ${error}`); + } + } + } + + subscribe(onChange: () => void): { unsubscribe: () => void } { + this.subscribers.push(onChange); + return { + unsubscribe: () => { + const index = this.subscribers.indexOf(onChange); + if (index >= 0) { + this.subscribers.splice(index, 1); + } + }, + }; + } + + has(key: string): boolean { + return this.config.has(key); + } + keys(): string[] { + return this.config.keys(); + } + get(key?: string): T { + return this.config.get(key); + } + getOptional(key?: string): T | undefined { + return this.config.getOptional(key); + } + getConfig(key: string): Config { + return this.config.getConfig(key); + } + getOptionalConfig(key: string): Config | undefined { + return this.config.getOptionalConfig(key); + } + getConfigArray(key: string): Config[] { + return this.config.getConfigArray(key); + } + getOptionalConfigArray(key: string): Config[] | undefined { + return this.config.getOptionalConfigArray(key); + } + getNumber(key: string): number { + return this.config.getNumber(key); + } + getOptionalNumber(key: string): number | undefined { + return this.config.getOptionalNumber(key); + } + getBoolean(key: string): boolean { + return this.config.getBoolean(key); + } + getOptionalBoolean(key: string): boolean | undefined { + return this.config.getOptionalBoolean(key); + } + getString(key: string): string { + return this.config.getString(key); + } + getOptionalString(key: string): string | undefined { + return this.config.getOptionalString(key); + } + getStringArray(key: string): string[] { + return this.config.getStringArray(key); + } + getOptionalStringArray(key: string): string[] | undefined { + return this.config.getOptionalStringArray(key); + } +} + +// A global used to ensure that only a single file watcher is active at a time. +let currentCancelFunc: () => void; + +/** + * Load configuration for a Backend. + * + * This function should only be called once, during the initialization of the backend. + * + * @public + */ +export async function loadBackendConfig(options: { logger: Logger; // process.argv or any other overrides argv: string[]; -}; - -/** - * Load configuration for a Backend - */ -export async function loadBackendConfig(options: Options): Promise { +}): Promise { const args = parseArgs(options.argv); - const configOpts: string[] = [args.config ?? []].flat(); + const configPaths: string[] = [args.config ?? []].flat(); + + const config = new ObservableConfigProxy(options.logger); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); + const configs = await loadConfig({ configRoot: paths.targetRoot, - configPaths: configOpts.map(opt => resolvePath(opt)), + configPaths: configPaths.map(opt => resolvePath(opt)), + watch: { + onChange(newConfigs) { + options.logger.info( + `Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`, + ); + + config.setConfig(ConfigReader.fromConfigs(newConfigs)); + }, + stopSignal: new Promise(resolve => { + if (currentCancelFunc) { + currentCancelFunc(); + } + currentCancelFunc = resolve; + + // For reloads of this module we need to use a dispose handler rather than the global. + if (module.hot) { + module.hot.addDisposeHandler(resolve); + } + }), + }, }); options.logger.info( `Loaded config from ${configs.map(c => c.context).join(', ')}`, ); - return ConfigReader.fromConfigs(configs); + config.setConfig(ConfigReader.fromConfigs(configs)); + + return config; } diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index f3a3f218b0..de39279a4f 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -31,6 +31,7 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } +/** @public */ export class DatabaseManager { /** * Creates a DatabaseManager from `backend.database` config. @@ -40,7 +41,7 @@ export class DatabaseManager { * defaults. Optionally, a user may set `prefix` which is used to prefix generated database * names if config is not provided. * - * @param config The loaded application configuration. + * @param config - The loaded application configuration. */ static fromConfig(config: Config): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); @@ -59,7 +60,7 @@ export class DatabaseManager { /** * Generates a PluginDatabaseManager for consumption by plugins. * - * @param pluginId The plugin that the database manager should be created for. Plugin names + * @param pluginId - The plugin that the database manager should be created for. Plugin names * should be unique as they are used to look up database config overrides under * `backend.database.plugin`. */ @@ -110,9 +111,7 @@ export class DatabaseManager { * @returns Object with client type returned as `client` and boolean representing whether * or not the client was overridden as `overridden` */ - private getClientType( - pluginId: string, - ): { + private getClientType(pluginId: string): { client: string; overridden: boolean; } { diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts index 153aea7f38..a81d60fe83 100644 --- a/packages/backend-common/src/database/SingleConnection.ts +++ b/packages/backend-common/src/database/SingleConnection.ts @@ -22,6 +22,7 @@ import { DatabaseManager } from './DatabaseManager'; * credentials provided; if the database already exists no attempt to create * the database will be made. * + * @public * @deprecated Use `DatabaseManager` from `@backend-common` instead. */ export const SingleConnectionDatabaseManager = DatabaseManager; diff --git a/packages/backend-common/src/database/config.ts b/packages/backend-common/src/database/config.ts index b771811e8c..cf821087d7 100644 --- a/packages/backend-common/src/database/config.ts +++ b/packages/backend-common/src/database/config.ts @@ -19,6 +19,7 @@ import { merge } from 'lodash'; /** * Merges database objects together * + * @public * @param config The base config. The input is not modified * @param overrides Any additional overrides */ diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 6fdb5554bd..8ea46070eb 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -40,8 +40,9 @@ const ConnectorMapping: Record = { /** * Creates a knex database connection * - * @param dbConfig The database config - * @param overrides Additional options to merge with the config + * @public + * @param dbConfig - The database config + * @param overrides - Additional options to merge with the config */ export function createDatabaseClient( dbConfig: Config, @@ -57,12 +58,16 @@ export function createDatabaseClient( /** * Alias for createDatabaseClient + * + * @public * @deprecated Use createDatabaseClient instead */ export const createDatabase = createDatabaseClient; /** * Ensures that the given databases all exist, creating them if they do not. + * + * @public */ export async function ensureDatabaseExists( dbConfig: Config, diff --git a/packages/backend-common/src/database/connectors/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts index f2f2298559..94dce508c7 100644 --- a/packages/backend-common/src/database/connectors/mysql.ts +++ b/packages/backend-common/src/database/connectors/mysql.ts @@ -149,7 +149,7 @@ export async function ensureMysqlDatabaseExists( ) { const admin = createMysqlDatabaseClient(dbConfig, { connection: { - database: (null as unknown) as string, + database: null as unknown as string, }, }); diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index c1647862af..edcba1e643 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -19,6 +19,8 @@ import { Knex } from 'knex'; /** * The PluginDatabaseManager manages access to databases that Plugins get. + * + * @public */ export interface PluginDatabaseManager { /** diff --git a/packages/backend-common/src/discovery/SingleHostDiscovery.ts b/packages/backend-common/src/discovery/SingleHostDiscovery.ts index 7d19284d9a..c1e092706b 100644 --- a/packages/backend-common/src/discovery/SingleHostDiscovery.ts +++ b/packages/backend-common/src/discovery/SingleHostDiscovery.ts @@ -26,6 +26,8 @@ import { DEFAULT_PORT } from '../service/lib/ServiceBuilderImpl'; * The deployment may be scaled horizontally, as long as the external URL * is the same for all instances. However, internal URLs will always be * resolved to the same host, so there won't be any balancing of internal traffic. + * + * @public */ export class SingleHostDiscovery implements PluginEndpointDiscovery { /** diff --git a/packages/backend-common/src/discovery/types.ts b/packages/backend-common/src/discovery/types.ts index a5915be773..0b502bf88c 100644 --- a/packages/backend-common/src/discovery/types.ts +++ b/packages/backend-common/src/discovery/types.ts @@ -25,6 +25,8 @@ * Implementations of the discovery API can be as simple as a URL pattern * using the pluginId, but could also have overrides for individual plugins, * or query a separate discovery service. + * + * @public */ export type PluginEndpointDiscovery = { /** diff --git a/packages/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts index 951e29da6a..7148778b8c 100644 --- a/packages/backend-common/src/hot.ts +++ b/packages/backend-common/src/hot.ts @@ -46,13 +46,14 @@ function findAllAncestors(_module: NodeModule): NodeModule[] { * * Useful for cleaning intervals, timers, requests etc * + * @public * @example * ```ts * const intervalId = setInterval(doStuff, 1000); * useHotCleanup(module, () => clearInterval(intervalId)); * ``` - * @param _module Reference to the current module where you invoke the fn - * @param cancelEffect Fn that cleans up the ongoing effects + * @param _module - Reference to the current module where you invoke the fn + * @param cancelEffect - Fn that cleans up the ongoing effects */ export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) { if (_module.hot) { @@ -78,16 +79,17 @@ const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key'; * Memoizes a generated value across hot-module reloads. This is useful for * stateful parts of the backend, e.g. to retain a database. * + * @public * @example * ```ts * const db = useHotMemoize(module, () => createDB(dbParams)); * ``` * - * @warning Don't use inside conditionals or loops, + * **NOTE:** Do not use inside conditionals or loops, * same rules as for hooks apply (https://reactjs.org/docs/hooks-rules.html) * - * @param _module Reference to the current module where you invoke the fn - * @param valueFactory Fn that returns the value you want to memoize + * @param _module - Reference to the current module where you invoke the fn + * @param valueFactory - Fn that returns the value you want to memoize */ export function useHotMemoize( _module: NodeModule, diff --git a/packages/backend-common/src/logging/formats.ts b/packages/backend-common/src/logging/formats.ts index 870eeafa2a..0477136e63 100644 --- a/packages/backend-common/src/logging/formats.ts +++ b/packages/backend-common/src/logging/formats.ts @@ -31,6 +31,7 @@ const coloredTemplate = (info: TransformableInfo) => { return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`; }; +/** @public */ export const coloredFormat = winston.format.combine( winston.format.timestamp(), winston.format.colorize({ diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index 58b675d9cf..d06037863c 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -21,14 +21,17 @@ import { coloredFormat } from './formats'; let rootLogger: winston.Logger; +/** @public */ export function getRootLogger(): winston.Logger { return rootLogger; } +/** @public */ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; } +/** @public */ export function createRootLogger( options: winston.LoggerOptions = {}, env = process.env, diff --git a/packages/backend-common/src/logging/voidLogger.ts b/packages/backend-common/src/logging/voidLogger.ts index 0afc1fc8c7..eb9993fe7f 100644 --- a/packages/backend-common/src/logging/voidLogger.ts +++ b/packages/backend-common/src/logging/voidLogger.ts @@ -19,6 +19,8 @@ import * as winston from 'winston'; /** * A logger that just throws away all messages. + * + * @public */ export function getVoidLogger(): winston.Logger { return winston.createLogger({ diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index ee7995f2c6..47c9285b18 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -28,6 +28,7 @@ import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; import { Logger } from 'winston'; import { getRootLogger } from '../logging'; +/** @public */ export type ErrorHandlerOptions = { /** * Whether error response bodies should show error stack traces or not. @@ -44,9 +45,9 @@ export type ErrorHandlerOptions = { logger?: Logger; /** - * Whether any error < 4XX should be logged or not. + * Whether any 4xx errors should be logged or not. * - * If not specified, by default log any 5xx errors. + * If not specified, default to only logging 5xx errors. */ logClientErrors?: boolean; }; @@ -62,6 +63,7 @@ export type ErrorHandlerOptions = { * error types (such as http-error exceptions) and returns the enclosed status * code accordingly. * + * @public * @returns An Express error request handler */ export function errorHandler( diff --git a/packages/backend-common/src/middleware/notFoundHandler.ts b/packages/backend-common/src/middleware/notFoundHandler.ts index 59ca957cc1..53a548c0ad 100644 --- a/packages/backend-common/src/middleware/notFoundHandler.ts +++ b/packages/backend-common/src/middleware/notFoundHandler.ts @@ -22,6 +22,7 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; * Should be used as the very last handler in the chain, as it unconditionally * returns a 404 status. * + * @public * @returns An Express request handler */ export function notFoundHandler(): RequestHandler { diff --git a/packages/backend-common/src/middleware/requestLoggingHandler.ts b/packages/backend-common/src/middleware/requestLoggingHandler.ts index f2f5cbda27..fa114411e1 100644 --- a/packages/backend-common/src/middleware/requestLoggingHandler.ts +++ b/packages/backend-common/src/middleware/requestLoggingHandler.ts @@ -22,7 +22,8 @@ import { getRootLogger } from '../logging'; /** * Logs incoming requests. * - * @param logger An optional logger to use. If not specified, the root logger will be used. + * @public + * @param logger - An optional logger to use. If not specified, the root logger will be used. * @returns An Express request handler */ export function requestLoggingHandler(logger?: Logger): RequestHandler { diff --git a/packages/backend-common/src/middleware/statusCheckHandler.ts b/packages/backend-common/src/middleware/statusCheckHandler.ts index 243d6533c8..a0ba59fac4 100644 --- a/packages/backend-common/src/middleware/statusCheckHandler.ts +++ b/packages/backend-common/src/middleware/statusCheckHandler.ts @@ -16,8 +16,10 @@ import { NextFunction, Request, Response, RequestHandler } from 'express'; +/** @public */ export type StatusCheck = () => Promise; +/** @public */ export interface StatusCheckHandlerOptions { /** * Optional status function which returns a message. @@ -30,7 +32,8 @@ export interface StatusCheckHandlerOptions { * * This is commonly used to implement healthcheck and readiness routes. * - * @param options An optional configuration object. + * @public + * @param options - An optional configuration object. * @returns An Express error request handler */ export async function statusCheckHandler( diff --git a/packages/backend-common/src/paths.ts b/packages/backend-common/src/paths.ts index ffe99fe762..c8a8849d6c 100644 --- a/packages/backend-common/src/paths.ts +++ b/packages/backend-common/src/paths.ts @@ -25,6 +25,8 @@ import { resolve as resolvePath } from 'path'; * This is particularly useful when you want to access assets shipped with * your backend plugin package. When doing so, do not forget to include the assets * in your published package by adding them to `files` in your `package.json`. + * + * @public */ export function resolvePackagePath(name: string, ...paths: string[]) { const req = @@ -40,8 +42,9 @@ export function resolvePackagePath(name: string, ...paths: string[]) { * a path that point to or within the base path. This is useful for resolving * paths from user input, as it otherwise opens up for vulnerabilities. * - * @param base The base directory to resolve the path from. - * @param path The target path, relative or absolute + * @public + * @param base - The base directory to resolve the path from. + * @param path - The target path, relative or absolute * @returns A path that is guaranteed to point to or within the base path. */ export function resolveSafeChildPath(base: string, path: string): string { diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts new file mode 100644 index 0000000000..084f7e3e13 --- /dev/null +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { ConfigReader, JsonObject } from '@backstage/config'; +import { getVoidLogger } from '../logging'; +import { DefaultReadTreeResponseFactory } from './tree'; +import { AwsS3UrlReader } from './AwsS3UrlReader'; +import { + AwsS3Integration, + readAwsS3IntegrationConfig, +} from '@backstage/integration'; +import { UrlReaderPredicateTuple } from './types'; +import AWSMock from 'aws-sdk-mock'; +import aws from 'aws-sdk'; +import path from 'path'; + +describe('AwsS3UrlReader', () => { + const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { + return AwsS3UrlReader.factory({ + config: new ConfigReader(config), + logger: getVoidLogger(), + treeResponseFactory: DefaultReadTreeResponseFactory.create({ + config: new ConfigReader({}), + }), + }); + }; + + afterEach(() => { + AWSMock.restore(); + }); + + it('creates a dummy reader without the awsS3 field', () => { + const entries = createReader({ + integrations: {}, + }); + + expect(entries).toHaveLength(1); + }); + + it('creates a reader with credentials correctly configured', () => { + const awsS3Integrations = []; + awsS3Integrations.push({ + host: 'amazonaws.com', + accessKeyId: 'fakekey', + secretAccessKey: 'fakekey', + }); + + const entries = createReader({ + integrations: { + awsS3: awsS3Integrations, + }, + }); + + expect(entries).toHaveLength(1); + }); + + it('creates a reader with default credentials provider', () => { + const awsS3Integrations = []; + awsS3Integrations.push({ + host: 'amazonaws.com', + }); + + const entries = createReader({ + integrations: { + awsS3: awsS3Integrations, + }, + }); + + expect(entries).toHaveLength(1); + }); + + describe('predicates', () => { + const readers = createReader({ + integrations: { + awsS3: [{}], + }, + }); + const predicate = readers[0].predicate; + + it('returns true for the correct aws s3 storage host', () => { + expect( + predicate(new URL('https://test-bucket.s3.us-east-2.amazonaws.com')), + ).toBe(true); + }); + + it('returns true for a url with the full path and the correct host', () => { + expect( + predicate( + new URL( + 'https://test-bucket.s3.us-east-2.amazonaws.com/team/service/catalog-info.yaml', + ), + ), + ).toBe(true); + }); + + it('returns false for an incorrect host', () => { + expect(predicate(new URL('https://amazon.com'))).toBe(false); + }); + + it('returns false for a completely different host', () => { + expect(predicate(new URL('https://storage.cloud.google.com'))).toBe( + false, + ); + }); + + it("returns true for a url with a bucket with '.'", () => { + expect( + predicate( + new URL( + 'https://test.bucket.s3.us-east-2.amazonaws.com/team/service/catalog-info.yaml', + ), + ), + ).toBe(true); + }); + }); + + describe('read', () => { + AWSMock.setSDKInstance(aws); + AWSMock.mock( + 'S3', + 'getObject', + Buffer.from( + require('fs').readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'awsS3-mock-object.yaml', + ), + ), + ), + ); + const s3 = new aws.S3(); + const awsS3UrlReader = new AwsS3UrlReader( + new AwsS3Integration( + readAwsS3IntegrationConfig( + new ConfigReader({ + host: 'amazonaws.com', + accessKeyId: 'fake-access-key', + secretAccessKey: 'fake-secret-key', + }), + ), + ), + s3, + ); + + it('returns contents of an object in a bucket', async () => { + const response = await awsS3UrlReader.read( + 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', + ); + expect(response.toString().trim()).toBe('site_name: Test'); + }); + + it('rejects unknown targets', async () => { + await expect( + awsS3UrlReader.read( + 'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml', + ), + ).rejects.toThrow( + Error( + `Could not retrieve file from S3: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`, + ), + ); + }); + }); + + describe('readUrl', () => { + AWSMock.setSDKInstance(aws); + + AWSMock.mock( + 'S3', + 'getObject', + Buffer.from( + require('fs').readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'awsS3-mock-object.yaml', + ), + ), + ), + ); + + const s3 = new aws.S3(); + + const awsS3UrlReader = new AwsS3UrlReader( + new AwsS3Integration( + readAwsS3IntegrationConfig( + new ConfigReader({ + host: 'amazonaws.com', + accessKeyId: 'fake-access-key', + secretAccessKey: 'fake-secret-key', + }), + ), + ), + s3, + ); + + it('returns contents of an object in a bucket', async () => { + const response = await awsS3UrlReader.readUrl( + 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', + ); + const buffer = await response.buffer(); + expect(buffer.toString().trim()).toBe('site_name: Test'); + }); + + it('rejects unknown targets', async () => { + await expect( + awsS3UrlReader.readUrl( + 'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml', + ), + ).rejects.toThrow( + Error( + `Could not retrieve file from S3: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`, + ), + ); + }); + }); +}); diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.ts b/packages/backend-common/src/reading/AwsS3UrlReader.ts new file mode 100644 index 0000000000..d119115930 --- /dev/null +++ b/packages/backend-common/src/reading/AwsS3UrlReader.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 aws, { Credentials, S3 } from 'aws-sdk'; +import { CredentialsOptions } from 'aws-sdk/lib/credentials'; +import { + ReaderFactory, + ReadTreeResponse, + ReadUrlOptions, + ReadUrlResponse, + SearchResponse, + UrlReader, +} from './types'; +import getRawBody from 'raw-body'; +import { AwsS3Integration, ScmIntegrations } from '@backstage/integration'; + +const parseURL = ( + url: string, +): { path: string; bucket: string; region: string } => { + let { host, pathname } = new URL(url); + + /** + * Removes the leading '/' from the pathname to be processed + * as a parameter by AWS S3 SDK getObject method. + */ + pathname = pathname.substr(1); + + /** + * Checks that the given URL is a valid S3 object url. + * Format of a Valid S3 URL: https://bucket-name.s3.Region.amazonaws.com/keyname + */ + const validHost = new RegExp( + /^[a-z\d][a-z\d\.-]{1,61}[a-z\d]\.s3\.[a-z\d-]+\.amazonaws.com$/, + ); + if (!validHost.test(host)) { + throw new Error(`not a valid AWS S3 URL: ${url}`); + } + + const [bucket] = host.split(/\.s3\.[a-z\d-]+\.amazonaws.com/); + host = host.substring(bucket.length); + const [, , region, ,] = host.split('.'); + + return { + path: pathname, + bucket: bucket, + region: region, + }; +}; + +export class AwsS3UrlReader implements UrlReader { + static factory: ReaderFactory = ({ config }) => { + const integrations = ScmIntegrations.fromConfig(config); + + return integrations.awsS3.list().map(integration => { + const creds = AwsS3UrlReader.buildCredentials(integration); + const s3 = new S3({ + apiVersion: '2006-03-01', + credentials: creds, + }); + const reader = new AwsS3UrlReader(integration, s3); + const predicate = (url: URL) => + url.host.endsWith(integration.config.host); + return { reader, predicate }; + }); + }; + + constructor( + private readonly integration: AwsS3Integration, + private readonly s3: S3, + ) {} + + /** + * If accesKeyId and secretAccessKey are missing, the standard credentials provider chain will be used: + * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html + */ + private static buildCredentials( + integration?: AwsS3Integration, + ): Credentials | CredentialsOptions | undefined { + if (!integration) { + return undefined; + } + + const accessKeyId = integration.config.accessKeyId; + const secretAccessKey = integration.config.secretAccessKey; + let explicitCredentials: Credentials | undefined; + + if (accessKeyId && secretAccessKey) { + explicitCredentials = new Credentials({ + accessKeyId, + secretAccessKey, + }); + } + + const roleArn = integration.config.roleArn; + if (roleArn) { + return new aws.ChainableTemporaryCredentials({ + masterCredentials: explicitCredentials, + params: { + RoleSessionName: 'backstage-aws-s3-url-reader', + RoleArn: roleArn, + }, + }); + } + + return explicitCredentials; + } + + async read(url: string): Promise { + const response = await this.readUrl(url); + return response.buffer(); + } + + async readUrl( + url: string, + options?: ReadUrlOptions, + ): Promise { + try { + const { path, bucket, region } = parseURL(url); + aws.config.update({ region: region }); + + let params; + if (options?.etag) { + params = { + Bucket: bucket, + Key: path, + IfNoneMatch: options.etag, + }; + } else { + params = { + Bucket: bucket, + Key: path, + }; + } + + const response = this.s3.getObject(params); + const buffer = await getRawBody(response.createReadStream()); + const etag = (await response.promise()).ETag; + + return { + buffer: async () => buffer, + etag: etag, + }; + } catch (e) { + throw new Error(`Could not retrieve file from S3: ${e.message}`); + } + } + + async readTree(): Promise { + throw new Error('AwsS3Reader does not implement readTree'); + } + + async search(): Promise { + throw new Error('AwsS3Reader does not implement search'); + } + + toString() { + const secretAccessKey = this.integration.config.secretAccessKey; + return `awsS3{host=${this.integration.config.host},authed=${Boolean( + secretAccessKey, + )}}`; + } +} diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 2b7c21d4ab..e014fa2e75 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -78,21 +78,17 @@ describe('AzureUrlReader', () => { it.each([ { - url: - 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', + url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', config: createConfig(), response: expect.objectContaining({ - url: - 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master', }), }, { - url: - 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', + url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', config: createConfig(), response: expect.objectContaining({ - url: - 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml', }), }, { @@ -129,14 +125,12 @@ describe('AzureUrlReader', () => { { url: 'https://api.com/a/b/blob/master/path/to/c.yaml', config: createConfig(), - error: - 'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + error: 'Azure URL must point to a git repository', }, { url: 'com/a/b/blob/master/path/to/c.yaml', config: createConfig(), - error: - 'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml', }, { url: '', diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index bc22854d4e..6015b044bc 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -23,11 +23,9 @@ import { ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; -import parseGitUrl from 'git-url-parse'; import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { stripFirstDirectoryFromPath } from './tree/util'; import { ReadTreeResponseFactory, ReaderFactory, @@ -40,6 +38,7 @@ import { ReadUrlResponse, } from './types'; +/** @public */ export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); @@ -70,7 +69,7 @@ export class AzureUrlReader implements UrlReader { // for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html if (response.ok && response.status !== 203) { - return Buffer.from(await response.text()); + return Buffer.from(await response.arrayBuffer()); } const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; @@ -128,28 +127,38 @@ export class AzureUrlReader implements UrlReader { throw new Error(message); } + // When downloading a zip archive from azure on a subpath we get an extra directory + // layer added at the top. With for example the file /a/b/c.txt and a download of + // /a/b, we'll see /b/c.txt in the zip archive. This picks out /b so that we can remove it. + let subpath; + const path = new URL(url).searchParams.get('path'); + if (path) { + subpath = path.split('/').filter(Boolean).slice(-1)[0]; + } + return await this.deps.treeResponseFactory.fromZipArchive({ - stream: (archiveAzureResponse.body as unknown) as Readable, + stream: archiveAzureResponse.body as unknown as Readable, etag: commitSha, filter: options?.filter, + subpath, }); } async search(url: string, options?: SearchOptions): Promise { - const { filepath } = parseGitUrl(url); - const matcher = new Minimatch(filepath); + const treeUrl = new URL(url); + + const path = treeUrl.searchParams.get('path'); + const matcher = path && new Minimatch(path.replace(/^\/+/, '')); // TODO(freben): For now, read the entire repo and filter through that. In // a future improvement, we could be smart and try to deduce that non-glob // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used // to get just that part of the repo. - const treeUrl = new URL(url); treeUrl.searchParams.delete('path'); - treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, ''); const tree = await this.readTree(treeUrl.toString(), { etag: options?.etag, - filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + filter: p => (matcher ? matcher.match(p) : true), }); const files = await tree.files(); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 2849bc3cd9..4965d5a370 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -43,6 +43,8 @@ import { /** * A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as * the one exposed by Bitbucket Cloud itself. + * + * @public */ export class BitbucketUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { @@ -60,13 +62,8 @@ export class BitbucketUrlReader implements UrlReader { private readonly integration: BitbucketIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { - const { - host, - apiBaseUrl, - token, - username, - appPassword, - } = integration.config; + const { host, apiBaseUrl, token, username, appPassword } = + integration.config; if (!apiBaseUrl) { throw new Error( @@ -91,7 +88,7 @@ export class BitbucketUrlReader implements UrlReader { } if (response.ok) { - return Buffer.from(await response.text()); + return Buffer.from(await response.arrayBuffer()); } const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`; @@ -138,7 +135,7 @@ export class BitbucketUrlReader implements UrlReader { } return await this.deps.treeResponseFactory.fromTarArchive({ - stream: (archiveBitbucketResponse.body as unknown) as Readable, + stream: archiveBitbucketResponse.body as unknown as Readable, subpath: filepath, etag: lastCommitShortHash, filter: options?.filter, diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index 169cbfbf66..e8c16c5f0a 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -15,12 +15,16 @@ */ import { ConfigReader } from '@backstage/config'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { msw } from '@backstage/test-utils'; +import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '../logging'; import { FetchUrlReader } from './FetchUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +const fetchUrlReader = new FetchUrlReader(); + describe('FetchUrlReader', () => { const worker = setupServer(); @@ -30,6 +34,39 @@ describe('FetchUrlReader', () => { jest.clearAllMocks(); }); + beforeEach(() => { + worker.use( + rest.get('https://backstage.io/some-resource', (req, res, ctx) => { + if (req.headers.get('if-none-match') === 'foo') { + return res( + ctx.status(304), + ctx.set('Content-Type', 'text/plain'), + ctx.set('etag', 'foo'), + ); + } + + return res( + ctx.status(200), + ctx.set('Content-Type', 'text/plain'), + ctx.set('etag', 'foo'), + ctx.body('content foo'), + ); + }), + ); + + worker.use( + rest.get('https://backstage.io/not-exists', (_req, res, ctx) => { + return res(ctx.status(404)); + }), + ); + + worker.use( + rest.get('https://backstage.io/error', (_req, res, ctx) => { + return res(ctx.status(500), ctx.body('An internal error occured')); + }), + ); + }); + it('factory should create a single entry with a predicate that matches config', async () => { const entries = FetchUrlReader.factory({ config: new ConfigReader({ @@ -70,4 +107,55 @@ describe('FetchUrlReader', () => { expect(predicate(new URL('https://a.examples.org:700/test'))).toBe(true); expect(predicate(new URL('https://a.b.examples.org:700/test'))).toBe(true); }); + + describe('read', () => { + it('should return etag from the response', async () => { + const buffer = await fetchUrlReader.read( + 'https://backstage.io/some-resource', + ); + expect(buffer.toString()).toBe('content foo'); + }); + + it('should throw NotFound if server responds with 404', async () => { + await expect( + fetchUrlReader.read('https://backstage.io/not-exists'), + ).rejects.toThrow(NotFoundError); + }); + + it('should throw Error if server responds with 500', async () => { + await expect( + fetchUrlReader.read('https://backstage.io/error'), + ).rejects.toThrow(Error); + }); + }); + + describe('readUrl', () => { + it('should throw NotModified if server responds with 304', async () => { + await expect( + fetchUrlReader.readUrl('https://backstage.io/some-resource', { + etag: 'foo', + }), + ).rejects.toThrow(NotModifiedError); + }); + + it('should return etag from the response', async () => { + const response = await fetchUrlReader.readUrl( + 'https://backstage.io/some-resource', + ); + expect(response.etag).toBe('foo'); + expect((await response.buffer()).toString()).toEqual('content foo'); + }); + + it('should throw NotFound if server responds with 404', async () => { + await expect( + fetchUrlReader.readUrl('https://backstage.io/not-exists'), + ).rejects.toThrow(NotFoundError); + }); + + it('should throw Error if server responds with 500', async () => { + await expect( + fetchUrlReader.readUrl('https://backstage.io/error'), + ).rejects.toThrow(Error); + }); + }); }); diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts index 30468158aa..732d3b9f59 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import fetch from 'cross-fetch'; -import { NotFoundError } from '@backstage/errors'; import { ReaderFactory, ReadTreeResponse, @@ -27,6 +27,8 @@ import { /** * A UrlReader that does a plain fetch of the URL. + * + * @public */ export class FetchUrlReader implements UrlReader { /** @@ -57,15 +59,34 @@ export class FetchUrlReader implements UrlReader { }; async read(url: string): Promise { + const response = await this.readUrl(url); + return response.buffer(); + } + + async readUrl( + url: string, + options?: ReadUrlOptions, + ): Promise { let response: Response; try { - response = await fetch(url); + response = await fetch(url, { + headers: { + ...(options?.etag && { 'If-None-Match': options.etag }), + }, + }); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } + if (response.status === 304) { + throw new NotModifiedError(); + } + if (response.ok) { - return Buffer.from(await response.text()); + return { + buffer: async () => Buffer.from(await response.arrayBuffer()), + etag: response.headers.get('ETag') ?? undefined, + }; } const message = `could not read ${url}, ${response.status} ${response.statusText}`; @@ -75,15 +96,6 @@ export class FetchUrlReader implements UrlReader { throw new Error(message); } - async readUrl( - url: string, - _options?: ReadUrlOptions, - ): Promise { - // TODO etag is not implemented yet. - const buffer = await this.read(url); - return { buffer: async () => buffer }; - } - async readTree(): Promise { throw new Error('FetchUrlReader does not implement readTree'); } diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index c8002cd3ce..f9717a862a 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -41,9 +41,9 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); -const mockCredentialsProvider = ({ +const mockCredentialsProvider = { getCredentials: jest.fn().mockResolvedValue({ headers: {} }), -} as unknown) as GithubCredentialsProvider; +} as unknown as GithubCredentialsProvider; const githubProcessor = new GithubUrlReader( new GitHubIntegration( diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index bccddc6838..1eed5764ef 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -39,14 +39,20 @@ import { ReadUrlResponse, } 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']; +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. + * + * @public */ export class GithubUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { @@ -87,15 +93,20 @@ export class GithubUrlReader implements UrlReader { url: string, options?: ReadUrlOptions, ): Promise { - const ghUrl = getGitHubFileFetchUrl(url, this.integration.config); - const { headers } = await this.deps.credentialsProvider.getCredentials({ + const credentials = await this.deps.credentialsProvider.getCredentials({ url, }); + const ghUrl = getGitHubFileFetchUrl( + url, + this.integration.config, + credentials, + ); + let response: Response; try { - response = await fetch(ghUrl.toString(), { + response = await fetch(ghUrl, { headers: { - ...headers, + ...credentials?.headers, ...(options?.etag && { 'If-None-Match': options.etag }), Accept: 'application/vnd.github.v3.raw', }, @@ -110,7 +121,7 @@ export class GithubUrlReader implements UrlReader { if (response.ok) { return { - buffer: async () => Buffer.from(await response.text()), + buffer: async () => Buffer.from(await response.arrayBuffer()), etag: response.headers.get('ETag') ?? undefined, }; } @@ -195,7 +206,7 @@ export class GithubUrlReader implements UrlReader { 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, + stream: archive.body as unknown as Readable, subpath, etag: sha, filter: options?.filter, @@ -258,9 +269,7 @@ export class GithubUrlReader implements UrlReader { })); } - private async getRepoDetails( - url: string, - ): Promise<{ + private async getRepoDetails(url: string): Promise<{ repo: GhRepoResponse; branch: GhBranchResponse; }> { diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 945444d8ed..51b211e698 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -108,36 +108,30 @@ describe('GitlabUrlReader', () => { it.each([ // Project URLs { - url: - 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', config: createConfig(), response: expect.objectContaining({ - url: - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', headers: expect.objectContaining({ 'private-token': '', }), }), }, { - url: - 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + url: 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', config: createConfig('0123456789'), response: expect.objectContaining({ - url: - 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + url: 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', headers: expect.objectContaining({ 'private-token': '0123456789', }), }), }, { - url: - 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup + url: 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup config: createConfig(), response: expect.objectContaining({ - url: - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', }), }, diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 6c272471e2..b78add6916 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -38,6 +38,7 @@ import { ReadUrlOptions, } from './types'; +/** @public */ export class GitlabUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); @@ -84,7 +85,7 @@ export class GitlabUrlReader implements UrlReader { if (response.ok) { return { - buffer: async () => Buffer.from(await response.text()), + buffer: async () => Buffer.from(await response.arrayBuffer()), etag: response.headers.get('ETag') ?? undefined, }; } @@ -170,7 +171,7 @@ export class GitlabUrlReader implements UrlReader { } return await this.deps.treeResponseFactory.fromTarArchive({ - stream: (archiveGitLabResponse.body as unknown) as Readable, + stream: archiveGitLabResponse.body as unknown as Readable, subpath: filepath, etag: commitSha, filter: options?.filter, diff --git a/packages/backend-common/src/reading/GoogleGcsUrlReader.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.ts index 9f3fbad302..f1684c2945 100644 --- a/packages/backend-common/src/reading/GoogleGcsUrlReader.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.ts @@ -48,6 +48,7 @@ const parseURL = ( }; }; +/** @public */ export class GoogleGcsUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, logger }) => { if (!config.has('integrations.googleGcs')) { diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts index 4a166cb517..bbe94a0fb4 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts @@ -52,7 +52,11 @@ export class UrlReaderPredicateMux implements UrlReader { } } - throw new NotAllowedError(`Reading from '${url}' is not allowed`); + throw new NotAllowedError( + `Reading from '${url}' is not allowed. ` + + `You may need to configure an integration for the target host, or add it ` + + `to the configured list of allowed hosts at 'backend.reading.allow'`, + ); } async readUrl( diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index 2b3a2f166c..7120a9570f 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -25,8 +25,10 @@ import { GitlabUrlReader } from './GitlabUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; import { FetchUrlReader } from './FetchUrlReader'; import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; +import { AwsS3UrlReader } from './AwsS3UrlReader'; -type CreateOptions = { +/** @public */ +export type UrlReadersOptions = { /** Root config object */ config: Config; /** Logger used by all the readers */ @@ -37,12 +39,14 @@ type CreateOptions = { /** * UrlReaders provide various utilities related to the UrlReader interface. + * + * @public */ export class UrlReaders { /** * Creates a UrlReader without any known types. */ - static create({ logger, config, factories }: CreateOptions): UrlReader { + static create({ logger, config, factories }: UrlReadersOptions): UrlReader { const mux = new UrlReaderPredicateMux(logger); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config, @@ -64,7 +68,7 @@ export class UrlReaders { * * Any additional factories passed will be loaded before the default ones. */ - static default({ logger, config, factories = [] }: CreateOptions) { + static default({ logger, config, factories = [] }: UrlReadersOptions) { return UrlReaders.create({ logger, config, @@ -74,6 +78,7 @@ export class UrlReaders { GithubUrlReader.factory, GitlabUrlReader.factory, GoogleGcsUrlReader.factory, + AwsS3UrlReader.factory, FetchUrlReader.factory, ]), }); diff --git a/packages/backend-common/src/reading/__fixtures__/awsS3-mock-object.yaml b/packages/backend-common/src/reading/__fixtures__/awsS3-mock-object.yaml new file mode 100644 index 0000000000..7470c0e8a3 --- /dev/null +++ b/packages/backend-common/src/reading/__fixtures__/awsS3-mock-object.yaml @@ -0,0 +1 @@ +site_name: Test diff --git a/packages/backend-common/src/reading/__fixtures__/mock-main.zip b/packages/backend-common/src/reading/__fixtures__/mock-main.zip index beee59d3a0..ae362448fc 100644 Binary files a/packages/backend-common/src/reading/__fixtures__/mock-main.zip and b/packages/backend-common/src/reading/__fixtures__/mock-main.zip differ diff --git a/packages/backend-common/src/reading/__fixtures__/mock-with-extra-root-dir.zip b/packages/backend-common/src/reading/__fixtures__/mock-with-extra-root-dir.zip new file mode 100644 index 0000000000..beee59d3a0 Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/mock-with-extra-root-dir.zip differ diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 4c601556d7..097f604987 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -19,10 +19,20 @@ export { BitbucketUrlReader } from './BitbucketUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export type { + ReaderFactory, + ReadTreeOptions, ReadTreeResponse, + ReadTreeResponseFactory, ReadTreeResponseFile, + ReadTreeResponseDirOptions, + ReadTreeResponseFactoryOptions, + ReadUrlOptions, + ReadUrlResponse, + SearchOptions, SearchResponse, SearchResponseFile, UrlReader, + UrlReaderPredicateTuple, } from './types'; export { UrlReaders } from './UrlReaders'; +export type { UrlReadersOptions } from './UrlReaders'; diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts index 912fddf965..5fdd633102 100644 --- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts +++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts @@ -18,7 +18,7 @@ import os from 'os'; import { Config } from '@backstage/config'; import { ReadTreeResponse, - FromArchiveOptions, + ReadTreeResponseFactoryOptions, ReadTreeResponseFactory, } from '../types'; import { TarArchiveResponse } from './TarArchiveResponse'; @@ -34,7 +34,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory { constructor(private readonly workDir: string) {} - async fromTarArchive(options: FromArchiveOptions): Promise { + async fromTarArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise { return new TarArchiveResponse( options.stream, options.subpath ?? '', @@ -44,7 +46,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory { ); } - async fromZipArchive(options: FromArchiveOptions): Promise { + async fromZipArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise { return new ZipArchiveResponse( options.stream, options.subpath ?? '', diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index a81add58f7..f7ab7f5349 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -28,7 +28,7 @@ import { import { stripFirstDirectoryFromPath } from './util'; // Tar types for `Parse` is not a proper constructor, but it should be -const TarParseStream = (Parse as unknown) as { new (): ParseStream }; +const TarParseStream = Parse as unknown as { new (): ParseStream }; const pipeline = promisify(pipelineCb); @@ -43,7 +43,7 @@ export class TarArchiveResponse implements ReadTreeResponse { private readonly subPath: string, private readonly workDir: string, public readonly etag: string, - private readonly filter?: (path: string) => boolean, + private readonly filter?: (path: string, info: { size: number }) => boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -92,7 +92,7 @@ export class TarArchiveResponse implements ReadTreeResponse { const path = relativePath.slice(this.subPath.length); if (this.filter) { - if (!this.filter(path)) { + if (!this.filter(path, { size: entry.remain })) { entry.resume(); return; } @@ -155,7 +155,7 @@ export class TarArchiveResponse implements ReadTreeResponse { tar.extract({ strip, cwd: dir, - filter: path => { + filter: (path, stat) => { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. const relativePath = stripFirstDirectoryFromPath(path); @@ -164,7 +164,7 @@ export class TarArchiveResponse implements ReadTreeResponse { } if (this.filter) { const innerPath = path.split('/').slice(strip).join('/'); - return this.filter(innerPath); + return this.filter(innerPath, { size: stat.size }); } return true; }, diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts index 659875286e..a54bac0c4a 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts @@ -22,11 +22,15 @@ import { ZipArchiveResponse } from './ZipArchiveResponse'; const archiveData = fs.readFileSync( resolvePath(__filename, '../../__fixtures__/mock-main.zip'), ); +const archiveDataWithExtraDir = fs.readFileSync( + resolvePath(__filename, '../../__fixtures__/mock-with-extra-root-dir.zip'), +); describe('ZipArchiveResponse', () => { beforeEach(() => { mockFs({ '/test-archive.zip': archiveData, + '/test-archive-with-extra-root-dir.zip': archiveDataWithExtraDir, '/tmp': mockFs.directory(), }); }); diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 45c6880a55..e07cedde87 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -24,7 +24,6 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; -import { stripFirstDirectoryFromPath } from './util'; /** * Wraps a zip archive stream into a tree response reader. @@ -37,7 +36,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { private readonly subPath: string, private readonly workDir: string, public readonly etag: string, - private readonly filter?: (path: string) => boolean, + private readonly filter?: (path: string, info: { size: number }) => boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -67,15 +66,17 @@ export class ZipArchiveResponse implements ReadTreeResponse { } private shouldBeIncluded(entry: Entry): boolean { - const strippedPath = stripFirstDirectoryFromPath(entry.path); - if (this.subPath) { - if (!strippedPath.startsWith(this.subPath)) { + if (!entry.path.startsWith(this.subPath)) { return false; } } if (this.filter) { - return this.filter(this.getInnerPath(entry.path)); + return this.filter(this.getInnerPath(entry.path), { + size: + (entry.vars as { uncompressedSize?: number }).uncompressedSize ?? + entry.vars.compressedSize, + }); } return true; } @@ -95,7 +96,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { if (this.shouldBeIncluded(entry)) { files.push({ - path: this.getInnerPath(stripFirstDirectoryFromPath(entry.path)), + path: this.getInnerPath(entry.path), content: () => entry.buffer(), }); } else { @@ -143,9 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { // Ignore directory entries since we handle that with the file entries // as a zip can have files with directories without directory entries if (entry.type === 'File' && this.shouldBeIncluded(entry)) { - const entryPath = this.getInnerPath( - stripFirstDirectoryFromPath(entry.path), - ); + const entryPath = this.getInnerPath(entry.path); const dirname = platformPath.dirname(entryPath); if (dirname) { await fs.mkdirp(platformPath.join(dir, dirname)); diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index 8efc833ead..b7e1ff823f 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -20,6 +20,8 @@ import { Config } from '@backstage/config'; /** * A generic interface for fetching plain data from URLs. + * + * @public */ export type UrlReader = { /* Used to read a single file and return its content. */ @@ -39,6 +41,7 @@ export type UrlReader = { search(url: string, options?: SearchOptions): Promise; }; +/** @public */ export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; reader: UrlReader; @@ -47,6 +50,8 @@ export type UrlReaderPredicateTuple = { /** * A factory function that can read config to construct zero or more * UrlReaders along with a predicate for when it should be used. + * + * @public */ export type ReaderFactory = (options: { config: Config; @@ -56,6 +61,8 @@ export type ReaderFactory = (options: { /** * An options object for readUrl operations. + * + * @public */ export type ReadUrlOptions = { /** @@ -74,6 +81,8 @@ export type ReadUrlOptions = { /** * A response object for readUrl operations. + * + * @public */ export type ReadUrlResponse = { /** @@ -90,6 +99,8 @@ export type ReadUrlResponse = { /** * An options object for readTree operations. + * + * @public */ export type ReadTreeOptions = { /** @@ -104,7 +115,7 @@ export type ReadTreeOptions = { * * If no filter is provided all files are extracted. */ - filter?(path: string): boolean; + filter?(path: string, info?: { size: number }): boolean; /** * An etag can be provided to check whether readTree's response has changed from a previous execution. @@ -120,8 +131,16 @@ export type ReadTreeOptions = { etag?: string; }; +/** @public */ +export type ReadTreeResponseDirOptions = { + /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ + targetDir?: string; +}; + /** * A response object for readTree operations. + * + * @public */ export type ReadTreeResponse = { /** @@ -142,20 +161,18 @@ export type ReadTreeResponse = { etag: string; }; -export type ReadTreeResponseDirOptions = { - /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ - targetDir?: string; -}; - /** * Represents a single file in a readTree response. + * + * @public */ export type ReadTreeResponseFile = { path: string; content(): Promise; }; -export type FromArchiveOptions = { +/** @public */ +export type ReadTreeResponseFactoryOptions = { // A binary stream of a tar archive. stream: Readable; // If unset, the files at the root of the tree will be read. @@ -164,16 +181,23 @@ export type FromArchiveOptions = { // etag of the blob etag: string; // Filter passed on from the ReadTreeOptions - filter?: (path: string) => boolean; + filter?: (path: string, info?: { size: number }) => boolean; }; +/** @public */ export interface ReadTreeResponseFactory { - fromTarArchive(options: FromArchiveOptions): Promise; - fromZipArchive(options: FromArchiveOptions): Promise; + fromTarArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; + fromZipArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; } /** * An options object for search operations. + * + * @public */ export type SearchOptions = { /** @@ -192,6 +216,8 @@ export type SearchOptions = { /** * The output of a search operation. + * + * @public */ export type SearchResponse = { /** @@ -207,6 +233,8 @@ export type SearchResponse = { /** * Represents a single file in a search response. + * + * @public */ export type SearchResponseFile = { /** diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts index 0b70edc45d..abbafc375d 100644 --- a/packages/backend-common/src/scm/git.test.ts +++ b/packages/backend-common/src/scm/git.test.ts @@ -123,9 +123,9 @@ describe('Git', () => { await git.clone({ url, dir }); - const { onAuth } = ((isomorphic.clone as unknown) as jest.Mock< - typeof isomorphic['clone'] - >).mock.calls[0][0]!; + const { onAuth } = ( + isomorphic.clone as unknown as jest.Mock + ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); }); @@ -190,9 +190,9 @@ describe('Git', () => { await git.fetch({ remote, dir }); - const { onAuth } = ((isomorphic.fetch as unknown) as jest.Mock< - typeof isomorphic['fetch'] - >).mock.calls[0][0]!; + const { onAuth } = ( + isomorphic.fetch as unknown as jest.Mock + ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); }); @@ -279,9 +279,9 @@ describe('Git', () => { await git.push({ remote, dir }); - const { onAuth } = ((isomorphic.push as unknown) as jest.Mock< - typeof isomorphic['push'] - >).mock.calls[0][0]!; + const { onAuth } = ( + isomorphic.push as unknown as jest.Mock + ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); }); diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index e786c279a1..00b103b240 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -31,6 +31,8 @@ From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub Azure 'notempty' token */ + +/** @public */ export class Git { private constructor( private readonly config: { diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts index 17d69e7082..a6ca25415e 100644 --- a/packages/backend-common/src/service/createServiceBuilder.ts +++ b/packages/backend-common/src/service/createServiceBuilder.ts @@ -15,10 +15,13 @@ */ import { ServiceBuilderImpl } from './lib/ServiceBuilderImpl'; +import { ServiceBuilder } from './types'; /** * Creates a new service builder. + * + * @public */ -export function createServiceBuilder(_module: NodeModule) { +export function createServiceBuilder(_module: NodeModule): ServiceBuilder { return new ServiceBuilderImpl(_module); } diff --git a/packages/backend-common/src/service/createStatusCheckRouter.ts b/packages/backend-common/src/service/createStatusCheckRouter.ts index fd794cc9c4..0d0f93f25c 100644 --- a/packages/backend-common/src/service/createStatusCheckRouter.ts +++ b/packages/backend-common/src/service/createStatusCheckRouter.ts @@ -19,7 +19,8 @@ import Router from 'express-promise-router'; import express from 'express'; import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware'; -export interface StatusCheckRouterOptions { +/** @public */ +export async function createStatusCheckRouter(options: { logger: Logger; path?: string; /** @@ -27,11 +28,7 @@ export interface StatusCheckRouterOptions { * Override this to implement your own logic for a health check. */ statusCheck?: StatusCheck; -} - -export async function createStatusCheckRouter( - options: StatusCheckRouterOptions, -): Promise { +}): Promise { const router = Router(); const { path = '/healthcheck', statusCheck } = options; diff --git a/packages/backend-common/src/service/index.ts b/packages/backend-common/src/service/index.ts index 4eb0bf4a5a..d01f25fad3 100644 --- a/packages/backend-common/src/service/index.ts +++ b/packages/backend-common/src/service/index.ts @@ -16,4 +16,4 @@ export { createServiceBuilder } from './createServiceBuilder'; export { createStatusCheckRouter } from './createStatusCheckRouter'; -export type { ServiceBuilder } from './types'; +export type { ServiceBuilder, RequestLoggingHandlerFactory } from './types'; diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 380d61abc8..ed16c9c9a7 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -27,9 +27,9 @@ import { getRootLogger } from '../../logging'; import { errorHandler, notFoundHandler, - requestLoggingHandler, + requestLoggingHandler as defaultRequestLoggingHandler, } from '../../middleware'; -import { ServiceBuilder } from '../types'; +import { RequestLoggingHandlerFactory, ServiceBuilder } from '../types'; import { CspOptions, HttpsSettings, @@ -65,6 +65,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private cspOptions: Record | undefined; private httpsSettings: HttpsSettings | undefined; private routers: [string, Router][]; + private requestLoggingHandler: RequestLoggingHandlerFactory | undefined; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -144,23 +145,26 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } + setRequestLoggingHandler( + requestLoggingHandler: RequestLoggingHandlerFactory, + ) { + this.requestLoggingHandler = requestLoggingHandler; + return this; + } + async start(): Promise { const app = express(); - const { - port, - host, - logger, - corsOptions, - httpsSettings, - helmetOptions, - } = this.getOptions(); + const { port, host, logger, corsOptions, httpsSettings, helmetOptions } = + this.getOptions(); app.use(helmet(helmetOptions)); if (corsOptions) { app.use(cors(corsOptions)); } app.use(compression()); - app.use(requestLoggingHandler(logger)); + app.use( + (this.requestLoggingHandler ?? defaultRequestLoggingHandler)(logger), + ); for (const [root, route] of this.routers) { app.use(root, route); } diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 70f62acfca..3765cbcfdd 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; +import { Config } from '@backstage/config'; import cors from 'cors'; import { Router, RequestHandler } from 'express'; import { Server } from 'http'; import { Logger } from 'winston'; -import { HttpsSettings } from './lib/config'; +/** @public */ export type ServiceBuilder = { /** * Sets the service parameters based on configuration. * - * @param config The configuration to read + * @param config - The configuration to read */ - loadConfig(config: ConfigReader): ServiceBuilder; + loadConfig(config: Config): ServiceBuilder; /** * Sets the port to listen on. @@ -36,7 +36,7 @@ export type ServiceBuilder = { * variable named PORT and use that if present, otherwise it picks a default * port (7000). * - * @param port The port to listen on + * @param port - The port to listen on */ setPort(port: number): ServiceBuilder; @@ -45,7 +45,7 @@ export type ServiceBuilder = { * * '' is express default, which listens to all interfaces. * - * @param host The host to listen on + * @param host - The host to listen on */ setHost(host: string): ServiceBuilder; @@ -54,7 +54,7 @@ export type ServiceBuilder = { * * If no logger is given, the default root logger is used. * - * @param logger A winston logger + * @param logger - A winston logger */ setLogger(logger: Logger): ServiceBuilder; @@ -64,7 +64,7 @@ export type ServiceBuilder = { * If this method is not called, the resulting service will not have any * built in CORS handling. * - * @param options Standard CORS options + * @param options - Standard CORS options */ enableCors(options: cors.CorsOptions): ServiceBuilder; @@ -73,20 +73,36 @@ export type ServiceBuilder = { * * If this method is not called, the resulting service will use sensible defaults * - * @param options Standard certificate options + * @param options - Standard certificate options */ - setHttpsSettings(settings: HttpsSettings): ServiceBuilder; + setHttpsSettings(settings: { + certificate: { key: string; cert: string } | { hostname: string }; + }): ServiceBuilder; /** * Adds a router (similar to the express .use call) to the service. * - * @param root The root URL to bind to (e.g. "/api/function1") - * @param router An express router + * @param root - The root URL to bind to (e.g. "/api/function1") + * @param router - An express router */ addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; + /** + * Set the request logging handler + * + * If no handler is given the default one is used + * + * @param requestLoggingHandler - a factory function that given a logger returns an handler + */ + setRequestLoggingHandler( + requestLoggingHandler: RequestLoggingHandlerFactory, + ): ServiceBuilder; + /** * Starts the server using the given settings. */ start(): Promise; }; + +/** @public */ +export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler; diff --git a/packages/backend-common/src/util/ContainerRunner.ts b/packages/backend-common/src/util/ContainerRunner.ts index 80ac4e3954..be861501c3 100644 --- a/packages/backend-common/src/util/ContainerRunner.ts +++ b/packages/backend-common/src/util/ContainerRunner.ts @@ -16,6 +16,7 @@ import { Writable } from 'stream'; +/** @public */ export type RunContainerOptions = { imageName: string; command?: string | string[]; @@ -24,8 +25,10 @@ export type RunContainerOptions = { mountDirs?: Record; workingDir?: string; envVars?: Record; + pullImage?: boolean; }; +/** @public */ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise; } diff --git a/packages/backend-common/src/util/DockerContainerRunner.test.ts b/packages/backend-common/src/util/DockerContainerRunner.test.ts index 0ed41d5cd3..8833e4518b 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.test.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.test.ts @@ -58,6 +58,7 @@ describe('DockerContainerRunner', () => { }); afterEach(() => { + jest.clearAllMocks(); mockFs.restore(); }); @@ -86,6 +87,17 @@ describe('DockerContainerRunner', () => { expect(mockDocker.run).toHaveBeenCalled(); }); + it('should not pull the docker container when pullImage is false', async () => { + await containerTaskApi.runContainer({ + imageName, + args, + pullImage: false, + }); + + expect(mockDocker.pull).not.toHaveBeenCalled(); + expect(mockDocker.run).toHaveBeenCalled(); + }); + it('should call the dockerClient run command with the correct arguments passed through', async () => { await containerTaskApi.runContainer({ imageName, diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index 6ec366eb94..da3ce66ec2 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -23,6 +23,7 @@ export type UserOptions = { User?: string; }; +/** @public */ export class DockerContainerRunner implements ContainerRunner { private readonly dockerClient: Docker; @@ -38,6 +39,7 @@ export class DockerContainerRunner implements ContainerRunner { mountDirs = {}, workingDir, envVars = {}, + pullImage = true, }: RunContainerOptions) { // Show a better error message when Docker is unavailable. try { @@ -48,15 +50,17 @@ export class DockerContainerRunner implements ContainerRunner { ); } - await new Promise((resolve, reject) => { - this.dockerClient.pull(imageName, {}, (err, stream) => { - if (err) return reject(err); - stream.pipe(logStream, { end: false }); - stream.on('end', () => resolve()); - stream.on('error', (error: Error) => reject(error)); - return undefined; + if (pullImage) { + await new Promise((resolve, reject) => { + this.dockerClient.pull(imageName, {}, (err, stream) => { + if (err) return reject(err); + stream.pipe(logStream, { end: false }); + stream.on('end', () => resolve()); + stream.on('error', (error: Error) => reject(error)); + return undefined; + }); }); - }); + } const userOptions: UserOptions = {}; if (process.getuid && process.getgid) { @@ -89,18 +93,17 @@ export class DockerContainerRunner implements ContainerRunner { Env.push(`${key}=${value}`); } - const [ - { Error: error, StatusCode: statusCode }, - ] = await this.dockerClient.run(imageName, args, logStream, { - Volumes, - HostConfig: { - Binds, - }, - ...(workingDir ? { WorkingDir: workingDir } : {}), - Entrypoint: command, - Env, - ...userOptions, - } as Docker.ContainerCreateOptions); + const [{ Error: error, StatusCode: statusCode }] = + await this.dockerClient.run(imageName, args, logStream, { + Volumes, + HostConfig: { + Binds, + }, + ...(workingDir ? { WorkingDir: workingDir } : {}), + Entrypoint: command, + Env, + ...userOptions, + } as Docker.ContainerCreateOptions); if (error) { throw new Error( diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 2faa5afbc3..6e10d188df 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/backend-test-utils +## 0.1.7 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/config@0.1.9 + - @backstage/backend-common@0.9.2 + - @backstage/cli@0.7.11 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.0 + - @backstage/config@0.1.8 + +## 0.1.5 + +### Patch Changes + +- 524045758: Make sure that the unique databases names generated by `TestDatabases.create()` + don't collide. +- Updated dependencies + - @backstage/backend-common@0.8.9 + - @backstage/cli@0.7.8 + ## 0.1.4 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 9ec11a58b7..1713f05d25 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.4", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.5", - "@backstage/cli": "^0.7.1", - "@backstage/config": "^0.1.5", + "@backstage/backend-common": "^0.9.2", + "@backstage/cli": "^0.7.11", + "@backstage/config": "^0.1.9", "knex": "^0.95.1", "mysql2": "^2.2.5", "pg": "^8.3.0", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.7.11", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts index 62612f3fcc..000e047c4d 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.test.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts @@ -71,7 +71,7 @@ describe('TestDatabases', () => { await input.insert({ x: 'y' }).into('a'); // Look for the mark - const database = 'backstage_plugin_db0'; + const database = input.client.config.connection.database; const output = knexFactory({ client: 'pg', connection: { host, port, user, password, database }, @@ -105,7 +105,7 @@ describe('TestDatabases', () => { await input.insert({ x: 'y' }).into('a'); // Look for the mark - const database = 'backstage_plugin_db0'; + const database = input.client.config.connection.database; const output = knexFactory({ client: 'pg', connection: { host, port, user, password, database }, @@ -139,7 +139,7 @@ describe('TestDatabases', () => { await input.insert({ x: 'y' }).into('a'); // Look for the mark - const database = 'backstage_plugin_db0'; + const database = input.client.config.connection.database; const output = knexFactory({ client: 'mysql2', connection: { host, port, user, password, database }, diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index c35202354d..243c6bbe52 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -16,6 +16,7 @@ import { DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { randomBytes } from 'crypto'; import { Knex } from 'knex'; import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; import { startMysqlContainer } from './startMysqlContainer'; @@ -30,11 +31,12 @@ import { /** * Encapsulates the creation of ephemeral test database instances for use * inside unit or integration tests. + * + * @public */ export class TestDatabases { private readonly instanceById: Map; private readonly supportedIds: TestDatabaseId[]; - private lastDatabaseIndex: number; /** * Creates an empty `TestDatabases` instance, and sets up Jest to clean up @@ -99,7 +101,6 @@ export class TestDatabases { private constructor(supportedIds: TestDatabaseId[]) { this.instanceById = new Map(); this.supportedIds = supportedIds; - this.lastDatabaseIndex = 0; } supports(id: TestDatabaseId): boolean { @@ -114,7 +115,7 @@ export class TestDatabases { * Returns a fresh, unique, empty logical database on an instance of the * given database ID platform. * - * @param id The ID of the database platform to use, e.g. 'POSTGRES_13' + * @param id - The ID of the database platform to use, e.g. 'POSTGRES_13' * @returns A `Knex` connection object */ async init(id: TestDatabaseId): Promise { @@ -142,7 +143,7 @@ export class TestDatabases { // Ensure that a unique logical database is created in the instance const connection = await instance.databaseManager - .forPlugin(String(`db${this.lastDatabaseIndex++}`)) + .forPlugin(`db${randomBytes(16).toString('hex')}`) .getClient(); instance.connections.push(connection); diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index b5516a19c0..d405f73738 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -19,6 +19,8 @@ import { Knex } from 'knex'; /** * The possible databases to test against. + * + * @public */ export type TestDatabaseId = | 'POSTGRES_13' @@ -38,33 +40,31 @@ export type Instance = { databaseManager: DatabaseManager; connections: Array; }; -export const allDatabases: Record< - TestDatabaseId, - TestDatabaseProperties -> = Object.freeze({ - POSTGRES_13: { - name: 'Postgres 13.x', - driver: 'pg', - dockerImageName: 'postgres:13', - connectionStringEnvironmentVariableName: - 'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING', - }, - POSTGRES_9: { - name: 'Postgres 9.x', - driver: 'pg', - dockerImageName: 'postgres:9', - connectionStringEnvironmentVariableName: - 'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING', - }, - MYSQL_8: { - name: 'MySQL 8.x', - driver: 'mysql2', - dockerImageName: 'mysql:8', - connectionStringEnvironmentVariableName: - 'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING', - }, - SQLITE_3: { - name: 'SQLite 3.x', - driver: 'sqlite3', - }, -}); +export const allDatabases: Record = + Object.freeze({ + POSTGRES_13: { + name: 'Postgres 13.x', + driver: 'pg', + dockerImageName: 'postgres:13', + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING', + }, + POSTGRES_9: { + name: 'Postgres 9.x', + driver: 'pg', + dockerImageName: 'postgres:9', + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING', + }, + MYSQL_8: { + name: 'MySQL 8.x', + driver: 'mysql2', + dockerImageName: 'mysql:8', + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING', + }, + SQLITE_3: { + name: 'SQLite 3.x', + driver: 'sqlite3', + }, + }); diff --git a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts index 617e9eb2a1..b411086728 100644 --- a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts +++ b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export function isDockerDisabledForTests() { // If we are not running in continuous integration, the default is to skip // the (relatively heavy, long running) docker based tests. If you want to diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 47edc5107f..9ca401a105 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,100 @@ # example-backend +## 0.2.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.13.6 + - @backstage/plugin-scaffolder-backend@0.15.3 + - @backstage/plugin-techdocs-backend@0.10.1 + - @backstage/plugin-auth-backend@0.3.24 + - @backstage/integration@0.6.3 + - @backstage/plugin-search-backend@0.2.6 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.3 + - @backstage/plugin-search-backend-module-pg@0.2.0 + - @backstage/plugin-search-backend-node@0.4.2 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + - example-app@0.2.44 + +## 0.2.43 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.0 + - @backstage/plugin-catalog-backend@0.13.5 + - @backstage/plugin-search-backend-module-pg@0.1.3 + - @backstage/plugin-auth-backend@0.3.23 + - @backstage/plugin-scaffolder-backend@0.15.2 + - @backstage/integration@0.6.2 + - @backstage/config@0.1.8 + - @backstage/plugin-kubernetes-backend@0.3.15 + - @backstage/plugin-techdocs-backend@0.10.0 + - @backstage/plugin-jenkins-backend@0.1.4 + - @backstage/plugin-app-backend@0.3.16 + - @backstage/plugin-badges-backend@0.1.9 + - @backstage/plugin-code-coverage-backend@0.1.10 + - @backstage/plugin-graphql-backend@0.1.9 + - @backstage/plugin-kafka-backend@0.2.9 + - @backstage/plugin-proxy-backend@0.2.12 + - @backstage/plugin-rollbar-backend@0.1.14 + - @backstage/plugin-scaffolder-backend-module-rails@0.1.5 + - @backstage/plugin-search-backend@0.2.5 + - @backstage/plugin-todo-backend@0.1.11 + - example-app@0.2.43 + +## 0.2.41 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.3.20 + - @backstage/integration@0.6.0 + - @backstage/plugin-scaffolder-backend@0.15.0 + - @backstage/backend-common@0.8.9 + - @backstage/plugin-kubernetes-backend@0.3.14 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.2 + - @backstage/plugin-search-backend-module-pg@0.1.1 + - @backstage/plugin-catalog-backend@0.13.2 + - @backstage/plugin-code-coverage-backend@0.1.9 + - @backstage/plugin-scaffolder-backend-module-rails@0.1.4 + - @backstage/plugin-techdocs-backend@0.9.2 + - @backstage/plugin-todo-backend@0.1.9 + - example-app@0.2.41 + +## 0.2.38 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-backend@0.3.11 + - @backstage/catalog-client@0.3.17 + - @backstage/plugin-auth-backend@0.3.18 + - @backstage/plugin-jenkins-backend@0.1.2 + - @backstage/backend-common@0.8.7 + - @backstage/plugin-techdocs-backend@0.9.0 + - @backstage/plugin-scaffolder-backend@0.14.1 + +## 0.2.37 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.8.6 + - @backstage/plugin-scaffolder-backend@0.14.0 + - @backstage/plugin-catalog-backend@0.13.0 + - @backstage/plugin-auth-backend@0.3.17 + - @backstage/plugin-scaffolder-backend-module-rails@0.1.3 + - @backstage/plugin-search-backend-node@0.4.0 + - @backstage/plugin-techdocs-backend@0.8.7 + - @backstage/plugin-app-backend@0.3.15 + - @backstage/plugin-kubernetes-backend@0.3.10 + - @backstage/plugin-rollbar-backend@0.1.13 + - example-app@0.2.37 + - @backstage/plugin-search-backend@0.2.3 + ## 0.2.36 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 4338f3e2b7..56b73e0591 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.36", + "version": "0.2.44", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,42 +27,47 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.8.5", - "@backstage/catalog-client": "^0.3.16", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", - "@backstage/integration": "^0.5.8", - "@backstage/plugin-app-backend": "^0.3.13", - "@backstage/plugin-auth-backend": "^0.3.16", - "@backstage/plugin-badges-backend": "^0.1.8", - "@backstage/plugin-catalog-backend": "^0.12.0", - "@backstage/plugin-code-coverage-backend": "^0.1.8", - "@backstage/plugin-graphql-backend": "^0.1.8", - "@backstage/plugin-kubernetes-backend": "^0.3.9", - "@backstage/plugin-kafka-backend": "^0.2.8", - "@backstage/plugin-proxy-backend": "^0.2.9", - "@backstage/plugin-rollbar-backend": "^0.1.11", - "@backstage/plugin-scaffolder-backend": "^0.13.0", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.2", - "@backstage/plugin-search-backend": "^0.2.2", - "@backstage/plugin-search-backend-node": "^0.3.0", - "@backstage/plugin-techdocs-backend": "^0.8.6", - "@backstage/plugin-todo-backend": "^0.1.8", + "@backstage/backend-common": "^0.9.1", + "@backstage/catalog-client": "^0.3.17", + "@backstage/catalog-model": "^0.9.1", + "@backstage/config": "^0.1.8", + "@backstage/integration": "^0.6.3", + "@backstage/plugin-app-backend": "^0.3.16", + "@backstage/plugin-auth-backend": "^0.3.24", + "@backstage/plugin-badges-backend": "^0.1.9", + "@backstage/plugin-catalog-backend": "^0.13.6", + "@backstage/plugin-code-coverage-backend": "^0.1.10", + "@backstage/plugin-graphql-backend": "^0.1.9", + "@backstage/plugin-jenkins-backend": "^0.1.4", + "@backstage/plugin-kubernetes-backend": "^0.3.15", + "@backstage/plugin-kafka-backend": "^0.2.9", + "@backstage/plugin-proxy-backend": "^0.2.12", + "@backstage/plugin-rollbar-backend": "^0.1.14", + "@backstage/plugin-scaffolder-backend": "^0.15.3", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", + "@backstage/plugin-search-backend": "^0.2.6", + "@backstage/plugin-search-backend-node": "^0.4.2", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.3", + "@backstage/plugin-search-backend-module-pg": "^0.2.0", + "@backstage/plugin-techdocs-backend": "^0.10.1", + "@backstage/plugin-todo-backend": "^0.1.11", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", - "azure-devops-node-api": "^10.2.2", + "azure-devops-node-api": "^11.0.1", "dockerode": "^3.2.1", - "example-app": "^0.2.36", + "example-app": "^0.2.44", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "express-prom-bundle": "^6.3.6", "knex": "^0.95.1", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", + "prom-client": "^13.2.0", "sqlite3": "^5.0.1", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.3", + "@backstage/cli": "^0.7.10", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 168f6aa2d9..78c6c4c20b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -36,6 +36,7 @@ import { } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; +import { metricsInit, metricsHandler } from './metrics'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import codeCoverage from './plugins/codecoverage'; @@ -50,6 +51,7 @@ import todo from './plugins/todo'; import graphql from './plugins/graphql'; import app from './plugins/app'; import badges from './plugins/badges'; +import jenkins from './plugins/jenkins'; import { PluginEnvironment } from './types'; function makeCreateEnv(config: Config) { @@ -71,6 +73,7 @@ function makeCreateEnv(config: Config) { } async function main() { + metricsInit(); const logger = getRootLogger(); logger.info( @@ -101,6 +104,7 @@ async function main() { const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); const appEnv = useHotMemoize(module, () => createEnv('app')); const badgesEnv = useHotMemoize(module, () => createEnv('badges')); + const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -116,11 +120,13 @@ async function main() { apiRouter.use('/proxy', await proxy(proxyEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); apiRouter.use('/badges', await badges(badgesEnv)); + apiRouter.use('/jenkins', await jenkins(jenkinsEnv)); apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) .loadConfig(config) .addRouter('', await healthcheck(healthcheckEnv)) + .addRouter('', metricsHandler()) .addRouter('/api', apiRouter) .addRouter('', await app(appEnv)); diff --git a/packages/backend/src/metrics.ts b/packages/backend/src/metrics.ts new file mode 100644 index 0000000000..8834022cd9 --- /dev/null +++ b/packages/backend/src/metrics.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { useHotCleanup } from '@backstage/backend-common'; +import { RequestHandler, Request } from 'express'; +import promBundle from 'express-prom-bundle'; +import prom from 'prom-client'; +import * as url from 'url'; + +/** + * Experimental Prometheus metrics used to benchmark the performance of the + * software catalog. Use this at your own risk. + */ +const rootRegEx = new RegExp('^/([^/]*)/.*'); +const apiRegEx = new RegExp('^/api/([^/]*)/.*'); + +function normalizePath(req: Request): string { + const path = url.parse(req.originalUrl || req.url).pathname || '/'; + + // Capture /api/ and the plugin name + if (apiRegEx.test(path)) { + return path.replace(apiRegEx, '/api/$1'); + } + + // Only the first path segment at root level + return path.replace(rootRegEx, '/$1'); +} + +export function metricsInit(): void { + prom.collectDefaultMetrics({ prefix: 'backstage_' }); +} + +/** + * Adds a /metrics endpoint, register default runtime metrics and instrument the router. + */ +export function metricsHandler(): RequestHandler { + // We can only initialize the metrics once and have to clean them up between hot reloads + useHotCleanup(module, () => prom.register.clear()); + + return promBundle({ + includeMethod: true, + includePath: true, + // Using includePath alone is problematic, as it will include path labels with high + // cardinality (e.g. path params). Instead we would have to template them. However, this + // is difficult, as every backend plugin might use different routes. Instead we only take + // the first directory of the path, to have at least an idea how each plugin performs: + normalizePath, + promClient: { collectDefaultMetrics: {} }, + }); +} diff --git a/packages/backend/src/plugins/jenkins.ts b/packages/backend/src/plugins/jenkins.ts new file mode 100644 index 0000000000..5420eb0df6 --- /dev/null +++ b/packages/backend/src/plugins/jenkins.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 { + createRouter, + DefaultJenkinsInfoProvider, +} from '@backstage/plugin-jenkins-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; + +export default async function createPlugin({ + logger, + config, + discovery, +}: PluginEnvironment): Promise { + const catalog = new CatalogClient({ discoveryApi: discovery }); + + return await createRouter({ + logger, + jenkinsInfoProvider: DefaultJenkinsInfoProvider.fromConfig({ + catalog, + config, + }), + }); +} diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 4a1e415c74..2da108608e 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -13,28 +13,67 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useHotCleanup } from '@backstage/backend-common'; +import { + PluginDatabaseManager, + useHotCleanup, +} from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; import { createRouter } from '@backstage/plugin-search-backend'; +import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch'; +import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; import { IndexBuilder, LunrSearchEngine, + SearchEngine, } from '@backstage/plugin-search-backend-node'; +import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { Logger } from 'winston'; import { PluginEnvironment } from '../types'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; + +async function createSearchEngine({ + logger, + database, + config, +}: { + logger: Logger; + database: PluginDatabaseManager; + config: Config; +}): Promise { + if (config.has('search.elasticsearch')) { + return await ElasticSearchSearchEngine.fromConfig({ + logger, + config, + }); + } + + if (await PgSearchEngine.supported(database)) { + return await PgSearchEngine.from({ database }); + } + + return new LunrSearchEngine({ logger }); +} export default async function createPlugin({ logger, discovery, + config, + database, }: PluginEnvironment) { // Initialize a connection to a search engine. - const searchEngine = new LunrSearchEngine({ logger }); + const searchEngine = await createSearchEngine({ config, logger, database }); const indexBuilder = new IndexBuilder({ logger, searchEngine }); // Collators are responsible for gathering documents known to plugins. This // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: new DefaultTechDocsCollator({ discovery, logger }), }); // The scheduler controls when documents are gathered from collators and sent diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 70b79b961f..b32007a3e2 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/catalog-client +## 0.3.19 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/catalog-model@0.9.2 + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + +## 0.3.18 + +### Patch Changes + +- 11c370af2: Support filtering entities via property existence + +## 0.3.17 + +### Patch Changes + +- 71c936eb6: Export `CatalogRequestOptions` type + ## 0.3.16 ### Patch Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 2abdeb8c75..f059174ba3 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -21,6 +21,9 @@ export type AddLocationResponse = { entities: Entity[]; }; +// @public (undocumented) +export const CATALOG_FILTER_EXISTS: unique symbol; + // @public (undocumented) export interface CatalogApi { // (undocumented) @@ -113,8 +116,8 @@ export class CatalogClient implements CatalogApi { // @public (undocumented) export type CatalogEntitiesRequest = { filter?: - | Record[] - | Record + | Record[] + | Record | undefined; fields?: string[] | undefined; }; @@ -124,6 +127,16 @@ export type CatalogListResponse = { items: T[]; }; +// @public (undocumented) +export type CatalogRequestOptions = { + token?: string; +}; + +// @public +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; + // @public export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index fbb37d2449..2040090fde 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.16", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,13 +29,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", - "@backstage/errors": "^0.1.1", + "@backstage/catalog-model": "^0.9.2", + "@backstage/config": "^0.1.9", + "@backstage/errors": "^0.1.2", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.7.3", + "@backstage/cli": "^0.7.11", "@types/jest": "^26.0.7", "msw": "^0.29.0" }, diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 4e3d95f99f..d2db98bb25 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; -import { CatalogListResponse } from './types/api'; +import { CATALOG_FILTER_EXISTS, CatalogListResponse } from './types/api'; import { DiscoveryApi } from './types/discovery'; const server = setupServer(); @@ -83,7 +83,7 @@ describe('CatalogClient', () => { server.use( rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { expect(req.url.search).toBe( - '?filter=a=1,b=2,b=3,%C3%B6=%3D&filter=a=2', + '?filter=a=1,b=2,b=3,%C3%B6=%3D&filter=a=2&filter=c', ); return res(ctx.json([])); }), @@ -100,6 +100,9 @@ describe('CatalogClient', () => { { a: '2', }, + { + c: CATALOG_FILTER_EXISTS, + }, ], }, { token }, @@ -113,7 +116,7 @@ describe('CatalogClient', () => { server.use( rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { - expect(req.url.search).toBe('?filter=a=1,b=2,b=3,%C3%B6=%3D'); + expect(req.url.search).toBe('?filter=a=1,b=2,b=3,%C3%B6=%3D,c'); return res(ctx.json([])); }), ); @@ -124,6 +127,7 @@ describe('CatalogClient', () => { a: '1', b: ['2', '3'], ö: '=', + c: CATALOG_FILTER_EXISTS, }, }, { token }, diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 5dfcddde0c..410d921d3b 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -26,6 +26,7 @@ import { import { ResponseError } from '@backstage/errors'; import fetch from 'cross-fetch'; import { + CATALOG_FILTER_EXISTS, AddLocationRequest, AddLocationResponse, CatalogApi, @@ -35,6 +36,7 @@ import { } from './types/api'; import { DiscoveryApi } from './types/discovery'; +/** @public */ export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; @@ -69,9 +71,13 @@ export class CatalogClient implements CatalogApi { const filterParts: string[] = []; for (const [key, value] of Object.entries(filterItem)) { for (const v of [value].flat()) { - filterParts.push( - `${encodeURIComponent(key)}=${encodeURIComponent(v)}`, - ); + if (v === CATALOG_FILTER_EXISTS) { + filterParts.push(encodeURIComponent(key)); + } else if (typeof v === 'string') { + filterParts.push( + `${encodeURIComponent(key)}=${encodeURIComponent(v)}`, + ); + } } } diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index ae3fd7b514..e78bd9d713 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -16,22 +16,29 @@ import { Entity, EntityName, Location } from '@backstage/catalog-model'; +/** @public */ +export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS'); + +/** @public */ export type CatalogEntitiesRequest = { filter?: - | Record[] - | Record + | Record[] + | Record | undefined; fields?: string[] | undefined; }; +/** @public */ export type CatalogListResponse = { items: T[]; }; +/** @public */ export type CatalogRequestOptions = { token?: string; }; +/** @public */ export interface CatalogApi { // Entities getEntities( @@ -70,6 +77,7 @@ export interface CatalogApi { ): Promise; } +/** @public */ export type AddLocationRequest = { type?: string; target: string; @@ -77,6 +85,7 @@ export type AddLocationRequest = { presence?: 'optional' | 'required'; }; +/** @public */ export type AddLocationResponse = { location: Location; entities: Entity[]; diff --git a/packages/catalog-client/src/types/discovery.ts b/packages/catalog-client/src/types/discovery.ts index 447998b3b8..19ee5ed19c 100644 --- a/packages/catalog-client/src/types/discovery.ts +++ b/packages/catalog-client/src/types/discovery.ts @@ -16,6 +16,8 @@ /** * This is a copy of the core DiscoveryApi, to avoid importing core. + * + * @public */ export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 842d0393a4..7963891a4c 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -20,5 +20,8 @@ export type { CatalogApi, CatalogEntitiesRequest, CatalogListResponse, + CatalogRequestOptions, } from './api'; +export type { DiscoveryApi } from './discovery'; +export { CATALOG_FILTER_EXISTS } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/packages/catalog-client/src/types/status.ts b/packages/catalog-client/src/types/status.ts index d2935b890e..38560a3791 100644 --- a/packages/catalog-client/src/types/status.ts +++ b/packages/catalog-client/src/types/status.ts @@ -17,6 +17,8 @@ /** * The entity `status.items[].type` for the status of the processing engine in * regards to an entity. + * + * @public */ export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 5dbbfd241f..6e6a7193c1 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/catalog-model +## 0.9.2 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + +## 0.9.1 + +### Patch Changes + +- 13dc3735c: Add an optional `metadata.title` field to all entity kinds. + + This used to be available on only the `Template` kind, and we have decided that the metadata block should be the same for all kinds. A title can be useful especially in large and complex catalogs where users have a tough time navigating or discerning among the entities. + + It also carries some risk. You do not want to end up giving a title that collides with an actual name, which at best leads to confusion and at worst could be a liability. We do not perform any collision detection in the catalog. If you want to disallow this facility you may want to add a small processor that makes sure it's not set. + + At the time of writing this message, only the scaffolder actually makes use of this field for display purposes. + ## 0.9.0 ### Minor Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 74027716de..407e18a409 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -143,7 +143,7 @@ export type EntityEnvelope = { // @public export function entityEnvelopeSchemaValidator< - T extends EntityEnvelope = EntityEnvelope + T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T; // @public @@ -168,6 +168,7 @@ export type EntityMeta = JsonObject & { generation?: number; name: string; namespace?: string; + title?: string; description?: string; labels?: Record; annotations?: Record; @@ -184,8 +185,8 @@ export type EntityName = { // @public (undocumented) export const EntityPolicies: { - allOf(policies: EntityPolicy[]): AllEntityPolicies; - oneOf(policies: EntityPolicy[]): AnyEntityPolicy; + allOf(policies: EntityPolicy[]): EntityPolicy; + oneOf(policies: EntityPolicy[]): EntityPolicy; }; // @public @@ -202,6 +203,12 @@ export type EntityRef = name: string; }; +// @public +export type EntityRefContext = { + defaultKind?: string; + defaultNamespace?: string; +}; + // @public export type EntityRelation = { type: string; @@ -240,9 +247,7 @@ export function generateUpdatedEntity(previous: Entity, next: Entity): Entity; export function getEntityName(entity: Entity): EntityName; // @public -export function getEntitySourceLocation( - entity: Entity, -): { +export function getEntitySourceLocation(entity: Entity): { type: string; target: string; }; @@ -402,9 +407,7 @@ export function parseEntityRef( }; // @public -export function parseLocationReference( - ref: string, -): { +export function parseLocationReference(ref: string): { type: string; target: string; }; @@ -533,10 +536,6 @@ export interface TemplateEntityV1beta2 extends Entity { // (undocumented) kind: 'Template'; // (undocumented) - metadata: EntityMeta & { - title?: string; - }; - // (undocumented) spec: { type: string; parameters?: JsonObject | JsonObject[]; @@ -611,5 +610,9 @@ export type Validators = { // @public export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; +// Warnings were encountered during analysis: +// +// src/entity/Entity.d.ts:38:5 - (ae-incompatible-release-tags) The symbol "status" is marked as @public, but its signature references "UNSTABLE_EntityStatus" which is marked as @alpha + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 13c3214252..a20fb235c6 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.9.0", + "version": "0.9.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.5", - "@backstage/errors": "^0.1.1", + "@backstage/config": "^0.1.9", + "@backstage/errors": "^0.1.2", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", "ajv": "^7.0.3", @@ -40,7 +40,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.2", + "@backstage/cli": "^0.7.11", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index eca5e4b77a..0fff68d569 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -52,11 +52,12 @@ class AnyEntityPolicy implements EntityPolicy { } } +/** @public */ export const EntityPolicies = { - allOf(policies: EntityPolicy[]) { + allOf(policies: EntityPolicy[]): EntityPolicy { return new AllEntityPolicies(policies); }, - oneOf(policies: EntityPolicy[]) { + oneOf(policies: EntityPolicy[]): EntityPolicy { return new AnyEntityPolicy(policies); }, }; diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 8843f10f5f..ca38fef183 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -21,6 +21,7 @@ import { UNSTABLE_EntityStatus } from './EntityStatus'; /** * The parts of the format that's common to all versions/kinds of entity. * + * @public * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ export type Entity = { @@ -62,6 +63,7 @@ export type Entity = { /** * Metadata fields common to all versions/kinds of entity. * + * @public * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ @@ -103,7 +105,13 @@ export type EntityMeta = JsonObject & { * The name of the entity. * * Must be unique within the catalog at any given point in time, for any - * given namespace + kind pair. + * given namespace + kind pair. This value is part of the technical + * identifier of the entity, and as such it will appear in URLs, database + * tables, entity references, and similar. It is subject to restrictions + * regarding what characters are allowed. + * + * If you want to use a different, more human readable string with fewer + * restrictions on it in user interfaces, see the `title` field below. */ name: string; @@ -112,6 +120,23 @@ export type EntityMeta = JsonObject & { */ namespace?: string; + /** + * A display name of the entity, to be presented in user interfaces instead + * of the `name` property above, when available. + * + * This field is sometimes useful when the `name` is cumbersome or ends up + * being perceived as overly technical. The title generally does not have + * as stringent format requirements on it, so it may contain special + * characters and be more explanatory. Do keep it very short though, and + * avoid situations where a title can be confused with the name of another + * entity, or where two entities share a title. + * + * Note that this is only for display purposes, and may be ignored by some + * parts of the code. Entity references still always make use of the `name` + * property, not the title. + */ + title?: string; + /** * A short (typically relatively few words, on one line) description of the * entity. @@ -143,6 +168,8 @@ export type EntityMeta = JsonObject & { /** * A relation of a specific type to another entity in the catalog. + * + * @public */ export type EntityRelation = { /** @@ -158,6 +185,8 @@ export type EntityRelation = { /** * Holds the relation data for entities. + * + * @public */ export type EntityRelationSpec = { /** @@ -178,6 +207,8 @@ export type EntityRelationSpec = { /** * A link to external information that is related to the entity. + * + * @public */ export type EntityLink = { /** diff --git a/packages/catalog-model/src/entity/EntityEnvelope.ts b/packages/catalog-model/src/entity/EntityEnvelope.ts index ed21a7d666..290738f491 100644 --- a/packages/catalog-model/src/entity/EntityEnvelope.ts +++ b/packages/catalog-model/src/entity/EntityEnvelope.ts @@ -18,6 +18,7 @@ * The envelope skeleton parts of an entity - whatever is necessary to be able * to give it a ref and pass to further validation / policy checking. * + * @public * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ export type EntityEnvelope = { diff --git a/packages/catalog-model/src/entity/EntityStatus.ts b/packages/catalog-model/src/entity/EntityStatus.ts index 92e1d1454f..8f79fbd615 100644 --- a/packages/catalog-model/src/entity/EntityStatus.ts +++ b/packages/catalog-model/src/entity/EntityStatus.ts @@ -18,6 +18,7 @@ import { SerializedError } from '@backstage/errors'; /** * The current status of the entity, as claimed by various sources. + * * @alpha */ export type UNSTABLE_EntityStatus = { diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index d46d839720..ff031fc1ae 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -16,11 +16,15 @@ /** * The namespace that entities without an explicit namespace fall into. + * + * @public */ export const ENTITY_DEFAULT_NAMESPACE = 'default'; /** * The keys of EntityMeta that are auto-generated. + * + * @public */ export const ENTITY_META_GENERATED_FIELDS = [ 'uid', @@ -30,6 +34,9 @@ export const ENTITY_META_GENERATED_FIELDS = [ /** * Annotations for linking to entity from catalog pages. + * + * @public */ export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; +/** @public */ export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index d05045fc78..eac55ea325 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -42,6 +42,7 @@ export { serializeEntityRef, stringifyEntityRef, } from './ref'; +export type { EntityRefContext } from './ref'; export { entityHasChanges, generateEntityEtag, diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts index 1750aece17..9511dfb05f 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts @@ -21,6 +21,8 @@ import { Entity } from '../Entity'; /** * Sets a default namespace if none was set. + * + * @public */ export class DefaultNamespaceEntityPolicy implements EntityPolicy { private readonly namespace: string; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index ee95fd16ea..8655116ec6 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -29,6 +29,8 @@ import { Entity } from '../Entity'; * * This does not take into account machine generated fields such as uid, etag * and generation. + * + * @public */ export class FieldFormatEntityPolicy implements EntityPolicy { private readonly validators: Validators; diff --git a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts index 6700a935d5..1c3f865434 100644 --- a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts @@ -21,6 +21,8 @@ const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; /** * Ensures that there are no foreign root fields in the entity. + * + * @public */ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { private readonly knownFields: string[]; diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index 21539aaba4..c263689446 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -58,7 +58,7 @@ describe('SchemaValidEntityPolicy', () => { // it('rejects wrong root type', async () => { - await expect(policy.enforce((7 as unknown) as Entity)).rejects.toThrow( + await expect(policy.enforce(7 as unknown as Entity)).rejects.toThrow( /object/, ); }); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index b5a4305a3c..721dc7d2dc 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -27,6 +27,8 @@ import { EntityPolicy } from './types'; * This should be the first policy in the list, to ensure that other downstream * policies can work with a structure that is at least valid in therms of the * typescript type. + * + * @public */ export class SchemaValidEntityPolicy implements EntityPolicy { private validate: ValidateFunction | undefined; diff --git a/packages/catalog-model/src/entity/policies/types.ts b/packages/catalog-model/src/entity/policies/types.ts index 6c7f47c332..ad0485e711 100644 --- a/packages/catalog-model/src/entity/policies/types.ts +++ b/packages/catalog-model/src/entity/policies/types.ts @@ -19,12 +19,14 @@ import type { Entity } from '../Entity'; /** * A policy for validation or mutation to be applied to entities as they are * entering the system. + * + * @public */ export type EntityPolicy = { /** * Applies validation or mutation on an entity. * - * @param entity The entity, as validated/mutated so far in the policy tree + * @param entity - The entity, as validated/mutated so far in the policy tree * @returns The incoming entity, or a mutated version of the same, or * undefined if this processor could not handle the entity * @throws An error if the entity should be rejected diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 91d8547c6b..c1b9383eeb 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -18,9 +18,7 @@ import { EntityName, EntityRef } from '../types'; import { ENTITY_DEFAULT_NAMESPACE } from './constants'; import { Entity } from './Entity'; -function parseRefString( - ref: string, -): { +function parseRefString(ref: string): { kind?: string; namespace?: string; name: string; @@ -43,7 +41,8 @@ function parseRefString( * Extracts the kind, namespace and name that form the name triplet of the * given entity. * - * @param entity An entity + * @public + * @param entity - An entity * @returns The complete entity name */ export function getEntityName(entity: Entity): EntityName { @@ -56,8 +55,10 @@ export function getEntityName(entity: Entity): EntityName { /** * The context of defaults that entity reference parsing happens within. + * + * @public */ -type EntityRefContext = { +export type EntityRefContext = { /** The default kind, if none is given in the reference */ defaultKind?: string; /** The default namespace, if none is given in the reference */ @@ -72,8 +73,9 @@ type EntityRefContext = { * otherwise specified as part of the options, and will throw an error if no * kind was specified in the input reference and no default kind was given. * - * @param ref The reference to parse - * @param context The context of defaults that the parsing happens within + * @public + * @param ref - The reference to parse + * @param context - The context of defaults that the parsing happens within * @returns A complete entity name */ export function parseEntityName( @@ -101,8 +103,9 @@ export function parseEntityName( * The options object can contain default values for the kind and namespace, * that will be used if the input reference did not specify any. * - * @param ref The reference to parse - * @param context The context of defaults that the parsing happens within + * @public + * @param ref - The reference to parse + * @param context - The context of defaults that the parsing happens within * @returns The compound form of the reference */ export function parseEntityRef( @@ -113,6 +116,7 @@ export function parseEntityRef( namespace: string; name: string; }; +/** @public */ export function parseEntityRef( ref: EntityRef, context?: { defaultKind: string }, @@ -121,6 +125,7 @@ export function parseEntityRef( namespace?: string; name: string; }; +/** @public */ export function parseEntityRef( ref: EntityRef, context?: { defaultNamespace: string }, @@ -172,8 +177,9 @@ export function parseEntityRef( * special/reserved characters, it outputs the string form, otherwise it * outputs the compound form. * + * @public * @deprecated Use `stringifyEntityRef` instead - * @param ref The reference to serialize + * @param ref - The reference to serialize * @returns The same reference on either string or compound form */ export function serializeEntityRef( @@ -221,7 +227,8 @@ export function serializeEntityRef( * all parts of the name to lowercase and inserts the default namespace if needed. * It is typically not the best way to represent the entity reference to the user. * - * @param ref The reference to serialize + * @public + * @param ref - The reference to serialize * @returns The same reference on either string or compound form */ export function stringifyEntityRef( @@ -250,9 +257,10 @@ export function stringifyEntityRef( * The comparison is case insensitive, and all of kind, namespace, and name * must match (after applying the optional context to the ref). * - * @param entity The entity to match - * @param ref A string or compound entity ref - * @param context An optional context of default kind and namespace, that apply + * @public + * @param entity - The entity to match + * @param ref - A string or compound entity ref + * @param context - An optional context of default kind and namespace, that apply * to the ref if given * @returns True if matching, false otherwise */ diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts index 84b6845347..314f3b7737 100644 --- a/packages/catalog-model/src/entity/util.ts +++ b/packages/catalog-model/src/entity/util.ts @@ -21,6 +21,7 @@ import { Entity } from './Entity'; /** * Generates a new random UID for an entity. * + * @public * @returns A string with enough randomness to uniquely identify an entity */ export function generateEntityUid(): string { @@ -30,6 +31,7 @@ export function generateEntityUid(): string { /** * Generates a new random Etag for an entity. * + * @public * @returns A string with enough randomness to uniquely identify an entity * revision */ @@ -48,8 +50,9 @@ export function generateEntityEtag(): string { * account. It only compares the actual input entity data, i.e. metadata and * spec. * - * @param previous The old state of the entity - * @param next The new state of the entity + * @public + * @param previous - The old state of the entity + * @param next - The new state of the entity */ export function entityHasChanges(previous: Entity, next: Entity): boolean { const e1 = lodash.cloneDeep(previous); @@ -98,8 +101,9 @@ export function entityHasChanges(previous: Entity, next: Entity): boolean { * The previous revision is expected to be a complete model loaded from the * catalog, including the uid, etag and generation fields. * - * @param previous The old state of the entity - * @param next The new state of the entity + * @public + * @param previous - The old state of the entity + * @param next - The new state of the entity * @returns An entity with the merged state of both */ export function generateUpdatedEntity(previous: Entity, next: Entity): Entity { diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 737037e6b2..310b6695eb 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/API.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface ApiEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'API'; @@ -30,6 +31,6 @@ export interface ApiEntityV1alpha1 extends Entity { }; } -export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const apiEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 4e08d4cce9..6777778409 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Component.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface ComponentEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Component'; @@ -33,6 +34,6 @@ export interface ComponentEntityV1alpha1 extends Entity { }; } -export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const componentEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index c2f39de321..fe777af24e 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Domain.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface DomainEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Domain'; @@ -26,6 +27,6 @@ export interface DomainEntityV1alpha1 extends Entity { }; } -export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const domainEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 28f1503223..f6aab856f0 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Group.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface GroupEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Group'; @@ -34,6 +35,6 @@ export interface GroupEntityV1alpha1 extends Entity { }; } -export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const groupEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index 4a37340bee..f2ae4bcc91 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Location.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface LocationEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Location'; @@ -28,6 +29,6 @@ export interface LocationEntityV1alpha1 extends Entity { }; } -export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const locationEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index c8f96b87e3..acc6ab5245 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Resource.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface ResourceEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Resource'; @@ -29,6 +30,6 @@ export interface ResourceEntityV1alpha1 extends Entity { }; } -export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const resourceEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 7c719566ad..5f90351480 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/System.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface SystemEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'System'; @@ -27,6 +28,6 @@ export interface SystemEntityV1alpha1 extends Entity { }; } -export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const systemEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index 2710fe1275..e708d4b387 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -15,16 +15,14 @@ */ import { JsonObject } from '@backstage/config'; -import type { Entity, EntityMeta } from '../entity/Entity'; +import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Template.v1beta2.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface TemplateEntityV1beta2 extends Entity { apiVersion: 'backstage.io/v1beta2'; kind: 'Template'; - metadata: EntityMeta & { - title?: string; - }; spec: { type: string; parameters?: JsonObject | JsonObject[]; @@ -40,6 +38,6 @@ export interface TemplateEntityV1beta2 extends Entity { }; } -export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const templateEntityV1beta2Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index 267a9f07fd..d719ab35ef 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/User.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface UserEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'User'; @@ -31,6 +32,6 @@ export interface UserEntityV1alpha1 extends Entity { }; } -export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( - schema, -); +/** @public */ +export const userEntityV1alpha1Validator = + ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/relations.ts b/packages/catalog-model/src/kinds/relations.ts index 57977e7168..7546d1508b 100644 --- a/packages/catalog-model/src/kinds/relations.ts +++ b/packages/catalog-model/src/kinds/relations.ts @@ -25,40 +25,60 @@ Naming rules for relations in priority order: /** * An ownership relation where the owner is usually an organizational * entity (user or group), and the other entity can be anything. + * + * @public */ export const RELATION_OWNED_BY = 'ownedBy'; +/** @public */ export const RELATION_OWNER_OF = 'ownerOf'; /** * A relation with an API entity, typically from a component + * + * @public */ export const RELATION_CONSUMES_API = 'consumesApi'; +/** @public */ export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; +/** @public */ export const RELATION_PROVIDES_API = 'providesApi'; +/** @public */ export const RELATION_API_PROVIDED_BY = 'apiProvidedBy'; /** * A relation denoting a dependency on another entity. + * + * @public */ export const RELATION_DEPENDS_ON = 'dependsOn'; +/** @public */ export const RELATION_DEPENDENCY_OF = 'dependencyOf'; /** * A parent/child relation to build up a tree, used for example to describe * the organizational structure between groups. + * + * @public */ export const RELATION_PARENT_OF = 'parentOf'; +/** @public */ export const RELATION_CHILD_OF = 'childOf'; /** * A membership relation, typically for users in a group. + * + * @public */ export const RELATION_MEMBER_OF = 'memberOf'; +/** @public */ export const RELATION_HAS_MEMBER = 'hasMember'; /** * A part/whole relation, typically for components in a system and systems * in a domain. + * + * @public */ export const RELATION_PART_OF = 'partOf'; +/** @public */ export const RELATION_HAS_PART = 'hasPart'; diff --git a/packages/catalog-model/src/kinds/types.ts b/packages/catalog-model/src/kinds/types.ts index 0ec0d313bb..f4bc151384 100644 --- a/packages/catalog-model/src/kinds/types.ts +++ b/packages/catalog-model/src/kinds/types.ts @@ -18,12 +18,14 @@ import { Entity } from '../entity'; /** * Validates entities of a certain kind. + * + * @public */ export type KindValidator = { /** * Validates the entity as a known entity kind. * - * @param entity The entity to validate + * @param entity - The entity to validate * @returns Resolves to true, if the entity was of a kind that was known and * handled by this validator, and was found to be valid. Resolves to false, * if the entity was not of a kind that was known by this validator. diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 137d36a0da..4bcabdb705 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +/** @public */ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; +/** @public */ export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; +/** @public */ export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 431ee71218..cf12031dc1 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -23,12 +23,14 @@ import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation'; * Note that the return type is not `LocationSpec`, because we do not want to * conflate the string form with the additional properties of that type. * - * @param ref A string-form location reference, e.g. 'url:https://host' - * @returns A location reference, e.g. { type: 'url', target: 'https://host' } + * @public + * @param ref - A string-form location reference, e.g. `'url:https://host'` + * @returns A location reference, e.g. `{ type: 'url', target: 'https://host' }` */ -export function parseLocationReference( - ref: string, -): { type: string; target: string } { +export function parseLocationReference(ref: string): { + type: string; + target: string; +} { if (typeof ref !== 'string') { throw new TypeError( `Unable to parse location reference '${ref}', unexpected argument ${typeof ref}`, @@ -66,8 +68,9 @@ export function parseLocationReference( * Note that the input type is not `LocationSpec`, because we do not want to * conflate the string form with the additional properties of that type. * - * @param ref A location reference, e.g. { type: 'url', target: 'https://host' } - * @returns A string-form location reference, e.g. 'url:https://host' + * @public + * @param ref - A location reference, e.g. `{ type: 'url', target: 'https://host' }` + * @returns A string-form location reference, e.g. `'url:https://host'` */ export function stringifyLocationReference(ref: { type: string; @@ -88,12 +91,15 @@ export function stringifyLocationReference(ref: { * Returns the source code location of the Entity, to the extent that one exists. * * If the returned location type is of type 'url', the target should be readable at least - * using the UrlReader from @backstage/backend-common. If it is not of type 'url', the caller + * using the UrlReader from `@backstage/backend-common`. If it is not of type 'url', the caller * needs to have explicit handling of each location type or signal that it is not supported. + * + * @public */ -export function getEntitySourceLocation( - entity: Entity, -): { type: string; target: string } { +export function getEntitySourceLocation(entity: Entity): { + type: string; + target: string; +} { const locationRef = entity.metadata?.annotations?.[SOURCE_LOCATION_ANNOTATION] ?? entity.metadata?.annotations?.[LOCATION_ANNOTATION]; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts index 9837ce384d..1d16a9047d 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/packages/catalog-model/src/location/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export type LocationSpec = { type: string; target: string; @@ -23,6 +24,7 @@ export type LocationSpec = { presence?: 'optional' | 'required'; }; +/** @public */ export type Location = { id: string; } & LocationSpec; diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts index 4857fc76bc..f6680d6eee 100644 --- a/packages/catalog-model/src/location/validation.ts +++ b/packages/catalog-model/src/location/validation.ts @@ -17,7 +17,10 @@ import * as yup from 'yup'; import { LocationSpec, Location } from './types'; -/** @deprecated */ +/** + * @public + * @deprecated Use JSONSchema or validators instead. + */ export const locationSpecSchema = yup .object({ type: yup.string().required(), @@ -27,7 +30,10 @@ export const locationSpecSchema = yup .noUnknown() .required(); -/** @deprecated */ +/** + * @public + * @deprecated Use JSONSchema or validators instead. + */ export const locationSchema = yup .object({ id: yup.string().required(), @@ -37,7 +43,10 @@ export const locationSchema = yup .noUnknown() .required(); -/** @deprecated */ +/** + * @public + * @deprecated Use JSONSchema or validators instead. + */ export const analyzeLocationSchema = yup .object<{ location: LocationSpec }>({ location: locationSpecSchema, diff --git a/packages/catalog-model/src/schema/EntityMeta.schema.json b/packages/catalog-model/src/schema/EntityMeta.schema.json index ff0f9c84a8..c834bfb113 100644 --- a/packages/catalog-model/src/schema/EntityMeta.schema.json +++ b/packages/catalog-model/src/schema/EntityMeta.schema.json @@ -53,6 +53,12 @@ "examples": ["default", "admin"], "minLength": 1 }, + "title": { + "type": "string", + "description": "A display name of the entity, to be presented in user interfaces instead of the name property, when available.", + "examples": ["React SSR Template"], + "minLength": 1 + }, "description": { "type": "string", "description": "A short (typically relatively few words, on one line) description of the entity." diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index b14cdb8ee0..d68eac936d 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -69,17 +69,6 @@ "kind": { "enum": ["Template"] }, - "metadata": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The nice display name for the template.", - "examples": ["React SSR Template"], - "minLength": 1 - } - } - }, "spec": { "type": "object", "required": ["type", "steps"], diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 50de5a1cea..1fbb3be95c 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -17,10 +17,13 @@ import { JsonValue } from '@backstage/config'; import { JSONSchema7 } from 'json-schema'; +/** @public */ export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; /** * A complete entity name, with the full kind-namespace-name triplet. + * + * @public */ export type EntityName = { kind: string; @@ -36,6 +39,8 @@ export type EntityName = { * * Left-out parts of the reference need to be handled by the application, * either by rejecting the reference or by falling back to default values. + * + * @public */ export type EntityRef = | string diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 7c9736baf7..5fcd2ee755 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -19,16 +19,18 @@ import lodash from 'lodash'; /** * Contains various helper validation and normalization functions that can be * composed to form a Validator. + * + * @public */ export class CommonValidatorFunctions { /** * Checks that the value is on the form or , and validates * those parts separately. * - * @param value The value to check - * @param separator The separator between parts - * @param isValidPrefix Checks that the part before the separator is valid, if present - * @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid + * @param value - The value to check + * @param separator - The separator between parts + * @param isValidPrefix - Checks that the part before the separator is valid, if present + * @param isValidSuffix - Checks that the part after the separator (or the entire value if there is no separator) is valid */ static isValidPrefixAndOrSuffix( value: unknown, @@ -53,7 +55,7 @@ export class CommonValidatorFunctions { /** * Checks that the value can be safely transferred as JSON. * - * @param value The value to check + * @param value - The value to check */ static isJsonSafe(value: unknown): boolean { try { @@ -66,7 +68,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid DNS subdomain name. * - * @param value The value to check + * @param value - The value to check * @see https://tools.ietf.org/html/rfc1123 */ static isValidDnsSubdomain(value: unknown): boolean { @@ -81,7 +83,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid DNS label. * - * @param value The value to check + * @param value - The value to check * @see https://tools.ietf.org/html/rfc1123 */ static isValidDnsLabel(value: unknown): boolean { @@ -96,7 +98,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid URL. * - * @param value The value to check + * @param value - The value to check */ static isValidUrl(value: unknown): boolean { if (typeof value !== 'string') { @@ -115,7 +117,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a non empty string value. * - * @param value The value to check + * @param value - The value to check */ static isValidString(value: unknown): boolean { return typeof value === 'string' && value?.trim()?.length >= 1; diff --git a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts index 86d27e7b13..2423c47bef 100644 --- a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts @@ -20,6 +20,7 @@ import { CommonValidatorFunctions } from './CommonValidatorFunctions'; * Contains validation functions that match the Kubernetes spec, usable to * build a catalog that is compatible with those rule sets. * + * @public * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set diff --git a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts index 2fe74ea2a4..daa44ffaf1 100644 --- a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts @@ -22,7 +22,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; /** * Creates a validation function that takes some arbitrary data, and either * returns that data cast to an {@link EntityEnvelope} (or the given subtype) - * if it matches that schema, or throws a {@link TypeError} describing the + * if it matches that schema, or throws a {@link globals#TypeError} describing the * errors. * * Note that this validator is only meant for applying the base schema checks; @@ -38,10 +38,11 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * - EntityMeta * - common# * + * @public * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entityEnvelopeSchemaValidator< - T extends EntityEnvelope = EntityEnvelope + T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T { const validate = compileAjvSchema( schema ? (schema as Schema) : entityEnvelopeSchema, diff --git a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts index a295fac205..535dba9842 100644 --- a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts @@ -22,20 +22,20 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * Creates a validation function that takes some arbitrary data, and either * returns that data cast to a `T` if it matches that schema, or `false` if the * schema apiVersion/kind didn't apply to that data, or throws a - * {@link TypeError} describing actual errors. + * {@link globals#TypeError} describing actual errors. * * This validator is highly specialized, in that it has special treatment of * the `kind` and `apiVersion` root keys. This only works if your schema has * their rule set to `"enum"`: * - *
- *   "apiVersion": {
- *      "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
- *   },
- *   "kind": {
- *     "enum": ["Group"]
- *   },
- * 
+ * ``` + * "apiVersion": { + * "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + * }, + * "kind": { + * "enum": ["Group"] + * }, + * ``` * * In the above example, the created validator will return `false` if and only * if the kind and/or apiVersion mismatch. @@ -52,6 +52,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * - EntityMeta * - common# * + * @public * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entityKindSchemaValidator( diff --git a/packages/catalog-model/src/validation/entitySchemaValidator.test.ts b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts index 3dfe307f92..b486de1df2 100644 --- a/packages/catalog-model/src/validation/entitySchemaValidator.test.ts +++ b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts @@ -30,6 +30,8 @@ describe('entitySchemaValidator', () => { generation: 13, name: 'test', namespace: 'ns', + title: 'My Component, Yay', + description: 'Yeah this is probably the best component so far', labels: { 'backstage.io/custom': 'ValueStuff', }, @@ -192,6 +194,21 @@ describe('entitySchemaValidator', () => { expect(() => validator(entity)).toThrow(/namespace/); }); + it('accepts missing title', () => { + delete entity.metadata.title; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad title type', () => { + entity.metadata.title = 7; + expect(() => validator(entity)).toThrow(/title/); + }); + + it('rejects empty title', () => { + entity.metadata.title = ''; + expect(() => validator(entity)).toThrow(/title/); + }); + it('accepts missing description', () => { delete entity.metadata.description; expect(() => validator(entity)).not.toThrow(); diff --git a/packages/catalog-model/src/validation/entitySchemaValidator.ts b/packages/catalog-model/src/validation/entitySchemaValidator.ts index 8683ab6de6..dd110efd75 100644 --- a/packages/catalog-model/src/validation/entitySchemaValidator.ts +++ b/packages/catalog-model/src/validation/entitySchemaValidator.ts @@ -22,7 +22,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; /** * Creates a validation function that takes some arbitrary data, and either * returns that data cast to an {@link Entity} (or the given subtype) if it - * matches that schema, or throws a {@link TypeError} describing the errors. + * matches that schema, or throws a {@link globals#TypeError} describing the errors. * * Note that this validator is only meant for applying the base schema checks; * it does not take custom policies or additional processor based validation @@ -37,6 +37,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * - EntityMeta * - common# * + * @public * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entitySchemaValidator( diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 0ddd9d1088..48eff566f2 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -30,6 +30,7 @@ const defaultValidators: Validators = { isValidTag: CommonValidatorFunctions.isValidDnsLabel, }; +/** @public */ export function makeValidator(overrides: Partial = {}): Validators { return { ...defaultValidators, diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index cfa9d845bc..23d639c166 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export type Validators = { isValidApiVersion(value: unknown): boolean; isValidKind(value: unknown): boolean; diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index 52a247f57a..8a2a903efd 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli-common +## 0.1.3 + +### Patch Changes + +- d1da88a19: Properly export all used types. + ## 0.1.2 ### Patch Changes diff --git a/packages/cli-common/api-report.md b/packages/cli-common/api-report.md index 7141d27f3a..f129fa3045 100644 --- a/packages/cli-common/api-report.md +++ b/packages/cli-common/api-report.md @@ -3,26 +3,26 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - // @public export function findPaths(searchDir: string): Paths; // @public export function isChildPath(base: string, path: string): boolean; -// @public (undocumented) +// @public export type Paths = { - ownDir: string; - ownRoot: string; - targetDir: string; - targetRoot: string; - resolveOwn: ResolveFunc; - resolveOwnRoot: ResolveFunc; - resolveTarget: ResolveFunc; - resolveTargetRoot: ResolveFunc; + ownDir: string; + ownRoot: string; + targetDir: string; + targetRoot: string; + resolveOwn: ResolveFunc; + resolveOwnRoot: ResolveFunc; + resolveTarget: ResolveFunc; + resolveTargetRoot: ResolveFunc; }; +// @public (undocumented) +export type ResolveFunc = (...paths: string[]) => string; // (No @packageDocumentation comment for this package) - ``` diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 16f10717f9..c643192df3 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.2", + "version": "0.1.3", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index 1f4f17fe9f..ed8158366d 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -16,4 +16,4 @@ export { findPaths } from './paths'; export { isChildPath } from './isChildPath'; -export type { Paths } from './paths'; +export type { Paths, ResolveFunc } from './paths'; diff --git a/packages/cli-common/src/isChildPath.ts b/packages/cli-common/src/isChildPath.ts index f48c92db32..f66a0cfa94 100644 --- a/packages/cli-common/src/isChildPath.ts +++ b/packages/cli-common/src/isChildPath.ts @@ -18,6 +18,8 @@ import { relative, isAbsolute } from 'path'; /** * Checks if path is the same as or a child path of base. + * + * @public */ export function isChildPath(base: string, path: string): boolean { const relativePath = relative(base, path); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 12a8484797..90239dfa4a 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -17,10 +17,15 @@ import fs from 'fs'; import { dirname, resolve as resolvePath } from 'path'; +/** @public */ export type ResolveFunc = (...paths: string[]) => string; -// Common paths and resolve functions used by the cli. -// Currently assumes it is being executed within a monorepo. +/** + * Common paths and resolve functions used by the cli. + * Currently assumes it is being executed within a monorepo. + * + * @public + */ export type Paths = { // Root dir of the cli itself, containing package.json ownDir: string; @@ -100,6 +105,7 @@ export function findOwnRootDir(ownDir: string) { /** * Find paths related to a package and its execution context. * + * @public * @example * * const paths = findPaths(__dirname) diff --git a/packages/cli/.snyk b/packages/cli/.snyk new file mode 100644 index 0000000000..612b4b0525 --- /dev/null +++ b/packages/cli/.snyk @@ -0,0 +1,70 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-ANSIHTML-1296849: + - 'webpack-dev-server > ansi-html': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-ANSIREGEX-1583908: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-CSSWHAT-1298035: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-BROWSERSLIST-1090194: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-ISSVG-1243891: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-ISSVG-1085627: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-POSTCSS-1255640: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-POSTCSS-1090595: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-WS-1296835: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-IMMER-1540542: + - '*': + reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z + + SNYK-JS-SETVALUE-1540541: + - '*': + reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z +patch: {} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a8cacedf22..f404b49e48 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/cli +## 0.7.11 + +### Patch Changes + +- 13895db37: Support importing font files in tests. + This fixes remaining issues from [#7019](https://github.com/backstage/backstage/pull/7019). +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config-loader@0.6.8 + - @backstage/config@0.1.9 + +## 0.7.10 + +### Patch Changes + +- 5e803edb8: Added support for importing font files. Imports in CSS via `url()` are supported for the final frontend bundle, but not for packages that are built for publishing. Module imports of fonts files from TypeScript are supported everywhere. +- b5118ff76: Updated dependencies + +## 0.7.9 + +### Patch Changes + +- f3bba3d2b: Remove debug logging +- 8ea1e96b3: Fix file path handling in diff commands on Windows. +- 2518aab58: Compensate for error formatting mismatch between Webpack 5 and react-dev-utils +- 1ac2961c3: Reintroduce Node.js shims that were removed in the Webpack 5 migration. +- 8d07a8b03: Add Buffer to `ProvidePlugin` since this is no longer provided in `webpack@5` +- fe506a0cf: Remove Webpack deprecation message when running build. +- 485438a56: Fix `backstage-cli backend:dev` argument passing +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/config-loader@0.6.7 + +## 0.7.8 + +### Patch Changes + +- c4ef9181a: Migrate to using `webpack@5` 🎉 + +## 0.7.7 + +### Patch Changes + +- 6aa7c3db7: bump node-tar version to the latest +- e9d3983ee: Keep track of filtered configuration values when running frontend in development mode. +- Updated dependencies + - @backstage/config@0.1.6 + - @backstage/config-loader@0.6.6 + +## 0.7.6 + +### Patch Changes + +- 9d40fcb1e: - Bumping `material-ui/core` version to at least `4.12.2` as they made some breaking changes in later versions which broke `Pagination` of the `Table`. + - Switching out `material-table` to `@material-table/core` for support for the later versions of `material-ui/core` + - This causes a minor API change to `@backstage/core-components` as the interface for `Table` re-exports the `prop` from the underlying `Table` components. + - `onChangeRowsPerPage` has been renamed to `onRowsPerPageChange` + - `onChangePage` has been renamed to `onPageChange` + - Migration guide is here: https://material-table-core.com/docs/breaking-changes + +## 0.7.5 + +### Patch Changes + +- 9a96b5da7: chore: bump `eslint` to `7.30.0` + +## 0.7.4 + +### Patch Changes + +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. +- Updated dependencies + - @backstage/config-loader@0.6.5 + ## 0.7.3 ### Patch Changes diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts index 879e9b0b05..0bd0922004 100644 --- a/packages/cli/asset-types/asset-types.d.ts +++ b/packages/cli/asset-types/asset-types.d.ts @@ -68,6 +68,26 @@ declare module '*.svg' { export default src; } +declare module '*.eot' { + const src: string; + export default src; +} + +declare module '*.woff' { + const src: string; + export default src; +} + +declare module '*.woff2' { + const src: string; + export default src; +} + +declare module '*.ttf' { + const src: string; + export default src; +} + declare module '*.css' { const classes: { readonly [key: string]: string }; export default classes; diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index e2ff338b4e..4e11a63a6b 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -80,9 +80,8 @@ async function getConfig() { transform: { '\\.esm\\.js$': require.resolve('./jestEsmTransform.js'), // See jestEsmTransform.js '\\.(js|jsx|ts|tsx)$': require.resolve('@sucrase/jest-plugin'), - '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve( - './jestFileTransform.js', - ), + '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': + require.resolve('./jestFileTransform.js'), '\\.(yaml)$': require.resolve('yaml-jest'), }, diff --git a/packages/cli/package.json b/packages/cli/package.json index a7032879bc..dd79f0df19 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.7.3", + "version": "0.7.11", "private": false, "publishConfig": { "access": "public" @@ -30,9 +30,9 @@ "dependencies": { "@babel/core": "^7.4.4", "@babel/plugin-transform-modules-commonjs": "^7.4.4", - "@backstage/cli-common": "^0.1.2", - "@backstage/config": "^0.1.5", - "@backstage/config-loader": "^0.6.4", + "@backstage/cli-common": "^0.1.3", + "@backstage/config": "^0.1.9", + "@backstage/config-loader": "^0.6.8", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", @@ -50,14 +50,13 @@ "@svgr/plugin-svgo": "5.4.x", "@svgr/rollup": "5.5.x", "@svgr/webpack": "5.5.x", - "@types/start-server-webpack-plugin": "^2.2.0", "@types/webpack-env": "^1.15.2", - "@types/webpack-node-externals": "^2.5.0", - "@typescript-eslint/eslint-plugin": "^v4.27.0", - "@typescript-eslint/parser": "^v4.27.0", + "@typescript-eslint/eslint-plugin": "^v4.30.0", + "@typescript-eslint/parser": "^v4.28.3", "@yarnpkg/lockfile": "^1.1.0", "babel-plugin-dynamic-import-node": "^2.3.3", "bfj": "^7.0.2", + "buffer": "^6.0.3", "chalk": "^4.0.0", "chokidar": "^3.3.1", "commander": "^6.1.0", @@ -65,7 +64,7 @@ "dashify": "^2.0.0", "diff": "^5.0.0", "esbuild": "^0.8.56", - "eslint": "^7.1.0", + "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-import": "^2.20.2", @@ -75,20 +74,20 @@ "eslint-plugin-react": "^7.12.4", "eslint-plugin-react-hooks": "^4.0.0", "express": "^4.17.1", - "file-loader": "^6.2.0", - "fork-ts-checker-webpack-plugin": "^6.2.9", + "fork-ts-checker-webpack-plugin": "^4.0.5", "fs-extra": "9.1.0", "handlebars": "^4.7.3", - "html-webpack-plugin": "^4.3.0", + "html-webpack-plugin": "^5.3.1", "inquirer": "^7.0.4", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", "json-schema": "^0.3.0", "lodash": "^4.17.19", - "mini-css-extract-plugin": "^0.9.0", + "mini-css-extract-plugin": "^1.4.1", + "node-libs-browser": "^2.2.1", "ora": "^5.3.0", "postcss": "^8.1.0", - "raw-loader": "^4.0.1", + "process": "^0.11.10", "react": "^16.0.0", "react-dev-utils": "^11.0.4", "react-hot-loader": "^4.12.21", @@ -100,17 +99,17 @@ "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", + "run-script-webpack-plugin": "^0.0.11", "semver": "^7.3.2", - "start-server-webpack-plugin": "^2.2.5", "style-loader": "^1.2.1", "sucrase": "^3.18.2", - "tar": "^6.0.1", - "terser-webpack-plugin": "^1.4.3", + "tar": "^6.1.2", + "terser-webpack-plugin": "^5.1.3", "ts-loader": "^8.0.17", "typescript": "^4.0.3", - "url-loader": "^4.1.0", - "webpack": "^4.41.6", - "webpack-dev-server": "3.11.0", + "util": "^0.12.3", + "webpack": "^5.48.0", + "webpack-dev-server": "4.0.0-rc.0", "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yaml-jest": "^1.0.5", @@ -118,36 +117,37 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.8.4", - "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.1.4", - "@backstage/core-plugin-api": "^0.1.3", - "@backstage/core-app-api": "^0.1.4", - "@backstage/dev-utils": "^0.2.0", - "@backstage/test-utils": "^0.1.14", - "@backstage/theme": "^0.2.8", + "@backstage/backend-common": "^0.9.2", + "@backstage/config": "^0.1.9", + "@backstage/core-components": "^0.4.1", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/core-app-api": "^0.1.12", + "@backstage/dev-utils": "^0.2.9", + "@backstage/test-utils": "^0.1.17", + "@backstage/theme": "^0.2.10", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", - "@types/html-webpack-plugin": "^3.2.2", "@types/http-proxy": "^1.17.4", "@types/inquirer": "^7.3.1", - "@types/mini-css-extract-plugin": "^1.2.2", "@types/mock-fs": "^4.13.0", "@types/node": "^14.14.32", - "@types/react-dev-utils": "^9.0.4", "@types/recursive-readdir": "^2.2.0", "@types/rollup-plugin-peer-deps-external": "^2.2.0", "@types/rollup-plugin-postcss": "^2.0.0", "@types/tar": "^4.0.3", - "@types/webpack": "^4.41.7", - "@types/webpack-dev-server": "^3.11.0", + "@types/terser-webpack-plugin": "^5.0.4", + "@types/webpack": "^5.28.0", + "@types/webpack-dev-server": "^3.11.5", "@types/yarnpkg__lockfile": "^1.1.4", "del": "^6.0.0", "mock-fs": "^4.13.0", "nodemon": "^2.0.2", "ts-node": "^10.0.0" }, + "resolutions": { + "@types/webpack-dev-server/@types/webpack": "^5.28.0" + }, "files": [ "asset-types", "templates", diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 85fbdd97a0..208042c719 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -59,6 +59,7 @@ export default async (cmd: Command) => { ...(await loadCliConfig({ args: cmd.config, fromPackage: name, + withFilteredKeys: true, })), }); diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index b9ba93bc62..a0200b7ce8 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -28,6 +28,7 @@ export default async (cmd: Command) => { ...(await loadCliConfig({ args: cmd.config, fromPackage: name, + withFilteredKeys: true, })), }); diff --git a/packages/cli/src/commands/testCommand.ts b/packages/cli/src/commands/testCommand.ts index d885e1aec9..24a4e06f9e 100644 --- a/packages/cli/src/commands/testCommand.ts +++ b/packages/cli/src/commands/testCommand.ts @@ -63,6 +63,13 @@ export default async (cmd: Command) => { (process.env as any).NODE_ENV = 'test'; } + // This is to have a consistent timezone for when running tests that involve checking + // the formatting of date/times. + // https://stackoverflow.com/questions/56261381/how-do-i-set-a-timezone-in-my-jest-config + if (!process.env.TZ) { + process.env.TZ = 'UTC'; + } + // eslint-disable-next-line jest/no-jest-import await require('jest').run(args); }; diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index 68d0766217..c542da6dfb 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -55,11 +55,9 @@ export default async (cmd: Command) => { lockfile.replaceVersions(result.newVersions); await lockfile.save(); } else { - const [ - newVersionsForbidden, - newVersionsAllowed, - ] = partition(result.newVersions, ({ name }) => - forbiddenDuplicatesFilter(name), + const [newVersionsForbidden, newVersionsAllowed] = partition( + result.newVersions, + ({ name }) => forbiddenDuplicatesFilter(name), ); if (newVersionsForbidden.length && !fix) { success = false; diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 60a5f0efca..3c95a87bdd 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -80,7 +80,17 @@ export const makeConfigs = async ( postcss(), forwardFileImports({ exclude: /\.icon\.svg$/, - include: [/\.svg$/, /\.png$/, /\.gif$/, /\.jpg$/, /\.jpeg$/], + include: [ + /\.svg$/, + /\.png$/, + /\.gif$/, + /\.jpg$/, + /\.jpeg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], }), json(), yaml(), diff --git a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts index aa6a6b4e2e..91615eddee 100644 --- a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts +++ b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts @@ -15,14 +15,14 @@ */ import { resolve as resolvePath } from 'path'; -import { ResolvePlugin } from 'webpack'; +import { WebpackPluginInstance } from 'webpack'; import { isChildPath } from '@backstage/cli-common'; import { LernaPackage } from './types'; // Enables proper resolution of packages when linking in external packages. // Without this the packages would depend on dependencies in the node_modules // of the external packages themselves, leading to module duplication -export class LinkedPackageResolvePlugin implements ResolvePlugin { +export class LinkedPackageResolvePlugin implements WebpackPluginInstance { constructor( private readonly targetModules: string, private readonly packages: LernaPackage[], diff --git a/packages/cli/src/lib/bundler/backend.ts b/packages/cli/src/lib/bundler/backend.ts index bddeac0de3..bd4ab42098 100644 --- a/packages/cli/src/lib/bundler/backend.ts +++ b/packages/cli/src/lib/bundler/backend.ts @@ -26,23 +26,16 @@ export async function serveBackend(options: BackendServeOptions) { isDev: true, }); - const compiler = webpack(config); - - const watcher = compiler.watch( - { - poll: true, - }, - (err: Error) => { - if (err) { - console.error(err); - } else console.log('Build succeeded'); - }, - ); + const compiler = webpack(config, (err: Error | undefined) => { + if (err) { + console.error(err); + } else console.log('Build succeeded'); + }); const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { - watcher.close(() => console.log('Stopped watcher')); + compiler.close(() => console.log('Stopped watcher')); // exit instead of resolve. The process is shutting down and resolving a promise here logs an error process.exit(); }); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 17a7398b2a..2f23d5cae6 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -69,6 +69,10 @@ export async function buildBundle(options: BuildOptions) { throw new Error(`Failed to compile.\n${error.message || error}`); }); + if (!stats) { + throw new Error('No stats returned'); + } + if (statsJsonEnabled) { // No @types/bfj await require('bfj').write( @@ -87,29 +91,49 @@ export async function buildBundle(options: BuildOptions) { } async function build(compiler: webpack.Compiler, isCi: boolean) { - const stats = await new Promise((resolve, reject) => { - compiler.run((err, buildStats) => { - if (err) { - if (err.message) { - const { errors } = formatWebpackMessages({ - errors: [err.message], - warnings: new Array(), - } as webpack.Stats.ToJsonOutput); + const stats = await new Promise( + (resolve, reject) => { + compiler.run((err, buildStats) => { + if (err) { + if (err.message) { + const { errors } = formatWebpackMessages({ + errors: [err.message], + warnings: new Array(), + _showErrors: true, + _showWarnings: true, + }); - throw new Error(errors[0]); + throw new Error(errors[0]); + } else { + reject(err); + } } else { - reject(err); + resolve(buildStats); } - } else { - resolve(buildStats); - } - }); - }); - - const { errors, warnings } = formatWebpackMessages( - stats.toJson({ all: false, warnings: true, errors: true }), + }); + }, ); + if (!stats) { + throw new Error('No stats provided'); + } + + const serializedStats = stats.toJson({ + all: false, + warnings: true, + errors: true, + }); + // NOTE(freben): The code below that extracts the message part of the errors, + // is due to react-dev-utils not yet being compatible with webpack 5. This + // may be possible to remove (just passing the serialized stats object + // directly into the format function) after a new release of react-dev-utils + // has been made available. + // See https://github.com/facebook/create-react-app/issues/9880 + const { errors, warnings } = formatWebpackMessages({ + errors: serializedStats.errors?.map(e => (e.message ? e.message : e)), + warnings: serializedStats.warnings?.map(e => (e.message ? e.message : e)), + }); + if (errors.length) { // Only keep the first error. Others are often indicative // of the same problem, but confuse the reader with noise. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 75e94af716..6e3f445f4a 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -19,8 +19,8 @@ import { resolve as resolvePath } from 'path'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; -import StartServerPlugin from 'start-server-webpack-plugin'; -import webpack from 'webpack'; +import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin'; +import webpack, { ProvidePlugin } from 'webpack'; import nodeExternals from 'webpack-node-externals'; import { isChildPath } from '@backstage/cli-common'; import { optimization } from './optimization'; @@ -32,6 +32,7 @@ import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types'; import { version } from '../../lib/version'; import { paths as cliPaths } from '../../lib/paths'; import { runPlain } from '../run'; +import pickBy from 'lodash/pickBy'; export function resolveBaseUrl(config: Config): URL { const baseUrl = config.getString('app.baseUrl'); @@ -98,10 +99,9 @@ export async function createConfig( if (checksEnabled) { plugins.push( new ForkTsCheckerWebpackPlugin({ - typescript: { - configFile: paths.targetTsConfig, - }, - eslint: { + typescript: paths.targetTsConfig, + eslint: true, + eslintOptions: { files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], options: { parserOptions: { @@ -114,6 +114,16 @@ export async function createConfig( ); } + // TODO(blam): process is no longer auto polyfilled by webpack in v5. + // we use the provide plugin to provide this polyfill, but lets look + // to remove this eventually! + plugins.push( + new ProvidePlugin({ + process: 'process/browser', + Buffer: ['buffer', 'Buffer'], + }), + ); + plugins.push( new webpack.EnvironmentPlugin({ APP_CONFIG: options.frontendAppConfigs, @@ -156,27 +166,34 @@ export async function createConfig( return { mode: isDev ? 'development' : 'production', profile: false, - node: { - module: 'empty', - dgram: 'empty', - dns: 'mock', - fs: 'empty', - http2: 'empty', - net: 'empty', - tls: 'empty', - child_process: 'empty', - }, optimization: optimization(options), bail: false, performance: { hints: false, // we check the gzip size instead }, - devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map', + devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map', context: paths.targetPath, entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], mainFields: ['browser', 'module', 'main'], + fallback: { + ...pickBy(require('node-libs-browser')), + module: false, + dgram: false, + dns: false, + fs: false, + http2: false, + net: false, + tls: false, + child_process: false, + + /* new ignores */ + path: false, + https: false, + http: false, + util: require.resolve('util/'), + }, plugins: [ new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs), new ModuleScopePlugin( @@ -194,13 +211,13 @@ export async function createConfig( output: { path: paths.targetDist, publicPath: validBaseUrl.pathname, - filename: isDev ? '[name].js' : 'static/[name].[hash:8].js', + filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', chunkFilename: isDev ? '[name].chunk.js' : 'static/[name].[chunkhash:8].chunk.js', ...(isDev ? { - devtoolModuleFilenameTemplate: info => + devtoolModuleFilenameTemplate: (info: any) => `file:///${resolvePath(info.absoluteResourcePath).replace( /\\/g, '/', @@ -237,7 +254,7 @@ export async function createBackendConfig( ? { watch: true, watchOptions: { - ignored: [/node_modules\/(?!\@backstage)/], + ignored: /node_modules\/(?!\@backstage)/, }, } : {}), @@ -259,7 +276,7 @@ export async function createBackendConfig( performance: { hints: false, // we check the gzip size instead }, - devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map', + devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map', context: paths.targetPath, entry: [ 'webpack/hot/poll?100', @@ -291,7 +308,7 @@ export async function createBackendConfig( : '[name].[chunkhash:8].chunk.js', ...(isDev ? { - devtoolModuleFilenameTemplate: info => + devtoolModuleFilenameTemplate: (info: any) => `file:///${resolvePath(info.absoluteResourcePath).replace( /\\/g, '/', @@ -300,18 +317,18 @@ export async function createBackendConfig( : {}), }, plugins: [ - new StartServerPlugin({ + new RunScriptWebpackPlugin({ name: 'main.js', nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined, + args: process.argv.slice(3), // drop `node backstage-cli backend:dev` }), new webpack.HotModuleReplacementPlugin(), ...(checksEnabled ? [ new ForkTsCheckerWebpackPlugin({ - typescript: { - configFile: paths.targetTsConfig, - }, - eslint: { + typescript: paths.targetTsConfig, + eslint: true, + eslintOptions: { files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], options: { parserOptions: { @@ -347,11 +364,10 @@ function nodeExternalsWithResolve( }); return ( - context: string, - request: string, - callback: webpack.ExternalsFunctionCallback, + { context, request }: { context?: string; request?: string }, + callback: any, ) => { - currentContext = context; + currentContext = context!; return externals(context, request, callback); }; } diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index e97b2ae868..b8164d263d 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Options } from 'webpack'; +import { WebpackOptionsNormalized, WebpackPluginInstance } from 'webpack'; import TerserPlugin from 'terser-webpack-plugin'; import { BundlingOptions } from './types'; import { isParallelDefault } from '../parallel'; export const optimization = ( options: BundlingOptions, -): Options.Optimization => { +): WebpackOptionsNormalized['optimization'] => { const { isDev } = options; return { @@ -32,7 +32,7 @@ export const optimization = ( minimizer: [ new TerserPlugin({ parallel: options.parallel, - }), + }) as unknown as WebpackPluginInstance, ], } : {}), diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 088128c805..2f8e3125c1 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -42,25 +42,31 @@ export async function serveBundle(options: ServeOptions) { }); const compiler = webpack(config); - const server = new WebpackDevServer(compiler, { - hot: !process.env.CI, - contentBase: paths.targetPublic, - contentBasePublicPath: config.output?.publicPath, - publicPath: config.output?.publicPath, - historyApiFallback: { - // Paths with dots should still use the history fallback. - // See https://github.com/facebookincubator/create-react-app/issues/387. - disableDotRule: true, - }, - clientLogLevel: 'warning', - stats: 'errors-warnings', - https: url.protocol === 'https:', - host, - port, - proxy: pkg.proxy, - // When the dev server is behind a proxy, the host and public hostname differ - allowedHosts: [url.hostname], - }); + const server = new WebpackDevServer( + compiler as any, + { + hot: !process.env.CI, + devMiddleware: { + publicPath: config.output?.publicPath as string, + stats: 'errors-warnings', + }, + static: { + publicPath: config.output?.publicPath as string, + directory: paths.targetPublic ?? '/', + }, + historyApiFallback: { + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, + }, + https: url.protocol === 'https:', + host, + port, + proxy: pkg.proxy, + // When the dev server is behind a proxy, the host and public hostname differ + allowedHosts: [url.hostname], + } as any, + ); await new Promise((resolve, reject) => { server.listen(port, host, (err?: Error) => { diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 6eb4920326..56162acf4b 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import webpack, { Module, Plugin } from 'webpack'; +import webpack, { ModuleOptions, WebpackPluginInstance } from 'webpack'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import { svgrTemplate } from '../svgrTemplate'; type Transforms = { - loaders: Module['rules']; - plugins: Plugin[]; + loaders: ModuleOptions['rules']; + plugins: WebpackPluginInstance[]; }; type TransformOptions = { @@ -51,6 +51,12 @@ export const transforms = (options: TransformOptions): Transforms => { production: !isDev, }, }, + { + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + }, { test: [/\.icon\.svg$/], use: [ @@ -77,10 +83,16 @@ export const transforms = (options: TransformOptions): Transforms => { { and: [/\.svg/, { not: [/\.icon\.svg/] }] }, /\.xml/, ], - loader: require.resolve('url-loader'), - options: { - limit: 10000, - name: 'static/[name].[hash:8].[ext]', + type: 'asset/resource', + generator: { + filename: 'static/[name].[hash:8].[ext]', + }, + }, + { + test: /\.(eot|woff|woff2|ttf)$/i, + type: 'asset/resource', + generator: { + filename: 'static/[name].[hash][ext][query]', }, }, { @@ -89,7 +101,10 @@ export const transforms = (options: TransformOptions): Transforms => { }, { include: /\.(md)$/, - use: require.resolve('raw-loader'), + type: 'asset/resource', + generator: { + filename: 'static/[name].[hash][ext][query]', + }, }, { test: /\.css$/i, @@ -105,7 +120,7 @@ export const transforms = (options: TransformOptions): Transforms => { }, ]; - const plugins = new Array(); + const plugins = new Array(); if (isDev) { plugins.push(new webpack.HotModuleReplacementPlugin()); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index db96ca7686..a93f040122 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -22,6 +22,7 @@ type Options = { args: string[]; fromPackage?: string; mockEnv?: boolean; + withFilteredKeys?: boolean; }; export async function loadCliConfig(options: Options) { @@ -57,6 +58,7 @@ export async function loadCliConfig(options: Options) { try { const frontendAppConfigs = schema.process(appConfigs, { visibility: ['frontend'], + withFilteredKeys: options.withFilteredKeys, }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index 5cbddde0ec..4a17125e5a 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -16,6 +16,7 @@ import chalk from 'chalk'; import { diffLines } from 'diff'; +import { sep, posix } from 'path'; import { FileDiff, PromptFunc, FileHandler, WriteFileFunc } from './types'; function sortObjectKeys(obj: Record) { @@ -281,7 +282,7 @@ export async function handleAllFiles( promptFunc: PromptFunc, ) { for (const file of files) { - const { path } = file; + const path = file.path.split(sep).join(posix.sep); const fileHandler = fileHandlers.find(handler => handler.patterns.some(pattern => typeof pattern === 'string' ? pattern === path : pattern.test(path), diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 7b23652c33..d3517e3c83 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -137,9 +137,9 @@ export class Lockfile { } // Find all versions currently in use - const versions = Array.from( - new Set(entries.map(e => e.version)), - ).sort((v1, v2) => semver.rcompare(v1, v2)); + const versions = Array.from(new Set(entries.map(e => e.version))).sort( + (v1, v2) => semver.rcompare(v1, v2), + ); // If we're not using at least 2 different versions we're done if (versions.length < 2) { diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 9088a86369..db5e508ec2 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -29,5 +29,230 @@ declare module '@svgr/rollup' { } declare module '@rollup/plugin-yaml'; +declare module 'react-dev-utils/formatWebpackMessages' { + export default function (stats: any): { + errors: string[]; + warnings: string[]; + }; +} -declare module 'terser-webpack-plugin'; +declare module 'react-dev-utils/openBrowser' { + export default function (url: string): boolean; +} + +declare module 'react-dev-utils/ModuleScopePlugin' { + import webpack = require('webpack'); + + export default class ModuleScopePlugin + implements webpack.WebpackPluginInstance + { + constructor( + appSrc: string | ReadonlyArray, + allowedFiles?: ReadonlyArray, + ); + apply: (resolver: any) => void; + } +} + +declare module 'react-dev-utils/FileSizeReporter' { + import webpack = require('webpack'); + + export interface OpaqueFileSizes { + root: string; + sizes: Record; + } + + /** + * Captures JS and CSS asset sizes inside the passed `buildFolder`. Save the + * result value to compare it after the build. + */ + export function measureFileSizesBeforeBuild( + buildFolder: string, + ): Promise; + + /** + * Prints the JS and CSS asset sizes after the build, and includes a size + * comparison with `previousFileSizes` that were captured earlier using + * `measureFileSizesBeforeBuild()`. `maxBundleGzipSize` and + * `maxChunkGzipSizemay` may optionally be specified to display a warning when + * the main bundle or a chunk exceeds the specified size (in bytes). + */ + export function printFileSizesAfterBuild( + webpackStats: webpack.Stats, + previousFileSizes: OpaqueFileSizes, + buildFolder: string, + maxBundleGzipSize?: number, + maxChunkGzipSize?: number, + ): void; +} + +declare module 'mini-css-extract-plugin' { + import webpack = require('webpack'); + + /** + * Lightweight CSS extraction webpack plugin. + * + * This plugin extracts CSS into separate files. It creates a CSS file per JS file which + * contains CSS. It supports On-Demand-Loading of CSS and SourceMaps. + * + * Configuration Detail: https://github.com/webpack-contrib/mini-css-extract-plugin#configuration + */ + export default class MiniCssExtractPlugin { + /** + * Webpack loader always used at the end of loaders list (ie. array index zero). + */ + static loader: string; + + constructor(options?: MiniCssExtractPlugin.PluginOptions); + + /** + * Apply the plugin + */ + apply(compiler: webpack.Compiler): void; + } + + namespace MiniCssExtractPlugin { + interface PluginOptions { + /** + * Works like [`output.filename`](https://webpack.js.org/configuration/output/#outputfilename). + */ + filename?: Required['output']['filename']; + /** + * Works like [`output.chunkFilename`](https://webpack.js.org/configuration/output/#outputchunkfilename). + */ + chunkFilename?: string; + /** + * For projects where CSS ordering has been mitigated through consistent + * use of scoping or naming conventions, the CSS order warnings can be + * disabled by setting this flag to true for the plugin. + */ + ignoreOrder?: boolean; + /** + * Specify where to insert the link tag. + * + * A string value specifies a DOM query for a parent element to attach to. + * + * A function allows to override default behavior for non-entry CSS chunks. + * This code will run in the browser alongside your application. It is recommend + * to only use ECMA 5 features and syntax. The function won't have access to the + * scope of the webpack configuration module. + * + * @default function() { document.head.appendChild(linkTag); } + */ + insert?: string | ((linkTag: any) => void); + /** + * Specify additional html attributes to add to the link tag. + * + * Note: These are only applied to dynamically loaded css chunks. To modify link + * attributes for entry CSS chunks, please use html-webpack-plugin. + */ + attributes?: Record; + /** + * This option allows loading asynchronous chunks with a custom link type, such as + * ``. + * + * `false` disables the link `type` attribute. + * + * @default 'text/css' + */ + linkType?: string | false | 'text/css'; + } + interface LoaderOptions { + /** + * Overrides [`output.publicPath`](https://webpack.js.org/configuration/output/#outputpublicpath). + * @default output.publicPath + */ + publicPath?: string | ((resourcePath: string, context: string) => string); + /** + * If false, the plugin will extract the CSS but **will not** emit the file + * @default true + */ + emit?: boolean; + /** + * By default, `mini-css-extract-plugin` generates JS modules that use the ES modules syntax. + * There are some cases in which using ES modules is beneficial, + * like in the case of module concatenation and tree shaking. + * @default true + */ + esModule?: boolean; + modules?: { + /** + * Enables/disables ES modules named export for locals. + * + * Names of locals are converted to camelCase. It is not allowed to use + * JavaScript reserved words in CSS class names. Options `esModule` and + * `modules.namedExport` in css-loader and MiniCssExtractPlugin.loader + * must be enabled. + * + * @default false + */ + namedExport?: boolean; + }; + } + } +} + +declare module 'fork-ts-checker-webpack-plugin/lib/ForkTsCheckerWebpackPlugin' {} +declare module 'webpack-node-externals' { + export default function webpackNodeExternals( + options?: webpackNodeExternals.Options, + ): any; + + namespace webpackNodeExternals { + type AllowlistOption = string | RegExp | AllowlistFunctionType; + type ImportTypeCallback = (moduleName: string) => string; + /** a function that accepts the module name and returns whether it should be included */ + type AllowlistFunctionType = (moduleName: string) => boolean; + interface ModulesFromFileType { + exclude?: string | string[]; + include?: string | string[]; + } + + interface Options { + /** + * An array for the externals to allow, so they will be included in the bundle. + * Can accept exact strings ('module_name'), regex patterns (/^module_name/), or a + * function that accepts the module name and returns whether it should be included. + * Important - if you have set aliases in your webpack config with the exact + * same names as modules in node_modules, you need to allowlist them so Webpack will know + * they should be bundled. + * @default [] + */ + allowlist?: AllowlistOption[] | AllowlistOption; + /** + * @default ['.bin'] + */ + binaryDirs?: string[]; + /** + * The method in which unbundled modules will be required in the code. Best to leave as + * 'commonjs' for node modules. + * @default 'commonjs' + */ + importType?: + | 'var' + | 'this' + | 'commonjs' + | 'amd' + | 'umd' + | ImportTypeCallback; + /** + * The folder in which to search for the node modules. + * @default 'node_modules' + */ + modulesDir?: string; + /** + * Additional folders to look for node modules. + */ + additionalModuleDirs?: string[]; + /** + * Read the modules from the package.json file instead of the node_modules folder. + * @default false + */ + modulesFromFile?: boolean | ModulesFromFileType; + /** + * @default false + */ + includeAbsolutePaths?: boolean; + } + } +} diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index dbba628055..fdac5a0c67 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -27,7 +27,7 @@ "@backstage/core-components": "^{{version '@backstage/core-components'}}", "@backstage/core-plugin-api": "^{{version '@backstage/core-plugin-api'}}", "@backstage/theme": "^{{version '@backstage/theme'}}", - "@material-ui/core": "^4.11.0", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index dc1f694dca..aa21ebc3a1 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/codemods +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.1 + - @backstage/cli-common@0.1.3 + - @backstage/core-app-api@0.1.12 + - @backstage/core-plugin-api@0.1.7 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.0 + - @backstage/core-app-api@0.1.11 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.10 + - @backstage/core-components@0.3.3 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.9 + - @backstage/core-components@0.3.2 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.8 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.3.0 + - @backstage/core-plugin-api@0.1.5 + - @backstage/core-app-api@0.1.7 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.2.0 + - @backstage/core-app-api@0.1.6 + - @backstage/core-plugin-api@0.1.4 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.1.6 + - @backstage/core-app-api@0.1.5 + ## 0.1.5 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 9cf5730126..0fbafe7fe2 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.5", + "version": "0.1.13", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "backstage-codemods": "bin/backstage-codemods" }, "dependencies": { - "@backstage/cli-common": "0.1.2", + "@backstage/cli-common": "0.1.3", "@backstage/core-app-api": "*", "@backstage/core-components": "*", "@backstage/core-plugin-api": "*", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index e0be1eebc8..88479bbb18 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/config-loader +## 0.6.8 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config@0.1.9 + +## 0.6.7 + +### Patch Changes + +- 0ade9d02b: Include `devDependencies` and `optionalDependencies` in the detection of Backstage packages when collecting configuration schema. +- 9b8cec063: Add support for config file watching through a new group of `watch` options to `loadConfig`. +- Updated dependencies + - @backstage/config@0.1.7 + +## 0.6.6 + +### Patch Changes + +- e9d3983ee: Add option to populate the `filteredKeys` property when processing configuration with a schema. +- Updated dependencies + - @backstage/config@0.1.6 + +## 0.6.5 + +### Patch Changes + +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. + ## 0.6.4 ### Patch Changes diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index ffd15b0c9a..a674f624b0 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -11,15 +11,25 @@ import { JSONSchema7 } from 'json-schema'; export type ConfigSchema = { process( appConfigs: AppConfig[], - options?: ConfigProcessingOptions, + options?: ConfigSchemaProcessingOptions, ): AppConfig[]; serialize(): JsonObject; }; // @public -export type ConfigVisibility = typeof CONFIG_VISIBILITIES[number]; +export type ConfigSchemaProcessingOptions = { + visibility?: ConfigVisibility[]; + valueTransform?: TransformFunc; + withFilteredKeys?: boolean; +}; + +// @public +export type ConfigVisibility = 'frontend' | 'backend' | 'secret'; // @public (undocumented) +export type EnvFunc = (name: string) => Promise; + +// @public export function loadConfig(options: LoadConfigOptions): Promise; // @public (undocumented) @@ -28,10 +38,25 @@ export type LoadConfigOptions = { configPaths: string[]; env?: string; experimentalEnvFunc?: EnvFunc; + watch?: { + onChange: (configs: AppConfig[]) => void; + stopSignal?: Promise; + }; }; // @public -export function loadConfigSchema(options: Options): Promise; +export function loadConfigSchema( + options: LoadConfigSchemaOptions, +): Promise; + +// @public (undocumented) +export type LoadConfigSchemaOptions = + | { + dependencies: string[]; + } + | { + serialized: JsonObject; + }; // @public export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7; @@ -41,5 +66,13 @@ export function readEnvConfig(env: { [name: string]: string | undefined; }): AppConfig[]; +// @public +export type TransformFunc = ( + value: T, + context: { + visibility: ConfigVisibility; + }, +) => T | undefined; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index cff324484a..0f5bac2ffc 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.6.4", + "version": "0.6.8", "private": false, "publishConfig": { "access": "public", @@ -30,10 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.1", - "@backstage/config": "^0.1.5", + "@backstage/cli-common": "^0.1.3", + "@backstage/config": "^0.1.9", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", + "chokidar": "^3.5.2", "fs-extra": "9.1.0", "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index f605d53115..29976b52de 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -15,6 +15,13 @@ */ export { readEnvConfig, loadConfigSchema, mergeConfigSchemas } from './lib'; -export type { ConfigSchema, ConfigVisibility } from './lib'; +export type { + ConfigSchema, + ConfigSchemaProcessingOptions, + ConfigVisibility, + EnvFunc, + LoadConfigSchemaOptions, + TransformFunc, +} from './lib'; export { loadConfig } from './loader'; export type { LoadConfigOptions } from './loader'; diff --git a/packages/config-loader/src/lib/env.ts b/packages/config-loader/src/lib/env.ts index 7d28e6b6ef..b244c06c64 100644 --- a/packages/config-loader/src/lib/env.ts +++ b/packages/config-loader/src/lib/env.ts @@ -38,6 +38,8 @@ const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; * For example, to set the config app.title to "My Title", use the following: * * APP_CONFIG_app_title='"My Title"' + * + * @public */ export function readEnvConfig(env: { [name: string]: string | undefined; diff --git a/packages/config-loader/src/lib/schema/collect.test.ts b/packages/config-loader/src/lib/schema/collect.test.ts index 479e63c94c..ef9d38faa9 100644 --- a/packages/config-loader/src/lib/schema/collect.test.ts +++ b/packages/config-loader/src/lib/schema/collect.test.ts @@ -87,6 +87,8 @@ describe('collectConfigSchemas', () => { dependencies: { c1: '0.0.0', c2: '0.0.0', + }, + devDependencies: { '@backstage/mock': '0.0.0', }, configSchema: { ...mockSchema, title: 'b' }, diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index b53389d499..b363e8e5f3 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -74,6 +74,8 @@ export async function collectConfigSchemas( const pkg = await fs.readJson(pkgPath); const depNames = [ ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ...Object.keys(pkg.optionalDependencies ?? {}), ...Object.keys(pkg.peerDependencies ?? {}), ]; diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index 4236fd2d17..a36d2eb9a2 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -108,6 +108,8 @@ export function compileConfigSchemas( /** * Given a list of configuration schemas from packages, merge them * into a single json schema. + * + * @public */ export function mergeConfigSchemas(schemas: JSONSchema[]): JSONSchema { const merged = mergeAllOf( diff --git a/packages/config-loader/src/lib/schema/filtering.test.ts b/packages/config-loader/src/lib/schema/filtering.test.ts index 5079afa876..d4b3cd55b6 100644 --- a/packages/config-loader/src/lib/schema/filtering.test.ts +++ b/packages/config-loader/src/lib/schema/filtering.test.ts @@ -63,43 +63,115 @@ const visibility = new Map( describe('filterByVisibility', () => { test.each<[ConfigVisibility[], JsonObject]>([ - [[], {}], + [ + [], + { + data: {}, + filteredKeys: [ + 'arr[0]', + 'arr[1]', + 'arr[2]', + 'objArr[0].f', + 'objArr[0].b', + 'objArr[0].s', + 'objArr[1].f', + 'objArr[1].b', + 'objArr[1].s', + 'obj.f', + 'obj.b.s', + 'arrF[0].never', + 'arrB[0].never', + 'arrS[0].never', + 'objF.never', + 'objB.never', + 'objS.never', + ], + }, + ], [ ['frontend'], { - arr: ['f'], - objArr: [{ f: 1 }, { f: 4 }], - obj: { f: 'a' }, - arrF: [], - objF: {}, + data: { + arr: ['f'], + objArr: [{ f: 1 }, { f: 4 }], + obj: { f: 'a' }, + arrF: [], + objF: {}, + }, + filteredKeys: [ + 'arr[1]', + 'arr[2]', + 'objArr[0].b', + 'objArr[0].s', + 'objArr[1].b', + 'objArr[1].s', + 'obj.b.s', + 'arrF[0].never', + 'arrB[0].never', + 'arrS[0].never', + 'objF.never', + 'objB.never', + 'objS.never', + ], }, ], [ ['backend'], { - arr: ['b'], - objArr: [{ b: 2 }, { b: 5 }], - obj: { b: {} }, - arrF: [{ never: 'here' }], - arrB: [{ never: 'here' }], - arrS: [{ never: 'here' }], - objF: { never: 'here' }, - objB: { never: 'here' }, - objS: { never: 'here' }, + data: { + arr: ['b'], + objArr: [{ b: 2 }, { b: 5 }], + obj: { b: {} }, + arrF: [{ never: 'here' }], + arrB: [{ never: 'here' }], + arrS: [{ never: 'here' }], + objF: { never: 'here' }, + objB: { never: 'here' }, + objS: { never: 'here' }, + }, + filteredKeys: [ + 'arr[0]', + 'arr[2]', + 'objArr[0].f', + 'objArr[0].s', + 'objArr[1].f', + 'objArr[1].s', + 'obj.f', + 'obj.b.s', + ], }, ], [ ['secret'], { - arr: ['s'], - objArr: [{ s: 3 }, { s: 6 }], - obj: { b: { s: true } }, - arrS: [], - objS: {}, + data: { + arr: ['s'], + objArr: [{ s: 3 }, { s: 6 }], + obj: { b: { s: true } }, + arrS: [], + objS: {}, + }, + filteredKeys: [ + 'arr[0]', + 'arr[1]', + 'objArr[0].f', + 'objArr[0].b', + 'objArr[1].f', + 'objArr[1].b', + 'obj.f', + 'arrF[0].never', + 'arrB[0].never', + 'arrS[0].never', + 'objF.never', + 'objB.never', + 'objS.never', + ], }, ], - [['frontend', 'backend', 'secret'], data], + [['frontend', 'backend', 'secret'], { data, filteredKeys: [] }], ])('should filter correctly with %p', (filter, expected) => { - expect(filterByVisibility(data, filter, visibility)).toEqual(expected); + expect( + filterByVisibility(data, filter, visibility, undefined, true), + ).toEqual(expected); }); }); diff --git a/packages/config-loader/src/lib/schema/filtering.ts b/packages/config-loader/src/lib/schema/filtering.ts index 74d367a30a..3533be01b7 100644 --- a/packages/config-loader/src/lib/schema/filtering.ts +++ b/packages/config-loader/src/lib/schema/filtering.ts @@ -30,9 +30,17 @@ export function filterByVisibility( includeVisibilities: ConfigVisibility[], visibilityByPath: Map, transformFunc?: TransformFunc, -): JsonObject { - function transform(jsonVal: JsonValue, path: string): JsonValue | undefined { - const visibility = visibilityByPath.get(path) ?? DEFAULT_CONFIG_VISIBILITY; + withFilteredKeys?: boolean, +): { data: JsonObject; filteredKeys?: string[] } { + const filteredKeys = new Array(); + + function transform( + jsonVal: JsonValue, + visibilityPath: string, // Matches the format we get from ajv + filterPath: string, // Matches the format of the ConfigReader + ): JsonValue | undefined { + const visibility = + visibilityByPath.get(visibilityPath) ?? DEFAULT_CONFIG_VISIBILITY; const isVisible = includeVisibilities.includes(visibility); if (typeof jsonVal !== 'object') { @@ -42,6 +50,9 @@ export function filterByVisibility( } return jsonVal; } + if (withFilteredKeys) { + filteredKeys.push(filterPath); + } return undefined; } else if (jsonVal === null) { return undefined; @@ -49,7 +60,11 @@ export function filterByVisibility( const arr = new Array(); for (const [index, value] of jsonVal.entries()) { - const out = transform(value, `${path}/${index}`); + const out = transform( + value, + `${visibilityPath}/${index}`, + `${filterPath}[${index}]`, + ); if (out !== undefined) { arr.push(out); } @@ -68,7 +83,11 @@ export function filterByVisibility( if (value === undefined) { continue; } - const out = transform(value, `${path}/${key}`); + const out = transform( + value, + `${visibilityPath}/${key}`, + filterPath ? `${filterPath}.${key}` : key, + ); if (out !== undefined) { outObj[key] = out; hasOutput = true; @@ -81,5 +100,8 @@ export function filterByVisibility( return undefined; } - return (transform(data, '') as JsonObject) ?? {}; + return { + filteredKeys: withFilteredKeys ? filteredKeys : undefined, + data: (transform(data, '', '') as JsonObject) ?? {}, + }; } diff --git a/packages/config-loader/src/lib/schema/index.ts b/packages/config-loader/src/lib/schema/index.ts index 851be36f84..1dcb9d7b4b 100644 --- a/packages/config-loader/src/lib/schema/index.ts +++ b/packages/config-loader/src/lib/schema/index.ts @@ -16,4 +16,10 @@ export { mergeConfigSchemas } from './compile'; export { loadConfigSchema } from './load'; -export type { ConfigSchema, ConfigVisibility } from './types'; +export type { LoadConfigSchemaOptions } from './load'; +export type { + ConfigSchema, + ConfigVisibility, + ConfigSchemaProcessingOptions, + TransformFunc, +} from './types'; diff --git a/packages/config-loader/src/lib/schema/load.test.ts b/packages/config-loader/src/lib/schema/load.test.ts index 4a2b719590..73fa55b90c 100644 --- a/packages/config-loader/src/lib/schema/load.test.ts +++ b/packages/config-loader/src/lib/schema/load.test.ts @@ -68,13 +68,19 @@ describe('loadConfigSchema', () => { schema.process(configs, { visibility: ['frontend'], valueTransform: () => 'X', + withFilteredKeys: true, }), - ).toEqual([{ data: { key1: 'X' }, context: 'test' }]); + ).toEqual([ + { data: { key1: 'X' }, context: 'test', filteredKeys: ['key2'] }, + ]); expect( schema.process(configs, { valueTransform: () => 'X', + withFilteredKeys: true, }), - ).toEqual([{ data: { key1: 'X', key2: 'X' }, context: 'test' }]); + ).toEqual([ + { data: { key1: 'X', key2: 'X' }, context: 'test', filteredKeys: [] }, + ]); const serialized = schema.serialize(); diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index ae5823c8cc..4620961310 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -24,7 +24,8 @@ import { CONFIG_VISIBILITIES, } from './types'; -type Options = +/** @public */ +export type LoadConfigSchemaOptions = | { dependencies: string[]; } @@ -34,9 +35,11 @@ type Options = /** * Loads config schema for a Backstage instance. + * + * @public */ export async function loadConfigSchema( - options: Options, + options: LoadConfigSchemaOptions, ): Promise { let schemas: ConfigSchemaPackageEntry[]; @@ -57,7 +60,7 @@ export async function loadConfigSchema( return { process( configs: AppConfig[], - { visibility, valueTransform } = {}, + { visibility, valueTransform, withFilteredKeys } = {}, ): AppConfig[] { const result = validate(configs); if (result.errors) { @@ -73,21 +76,23 @@ export async function loadConfigSchema( if (visibility) { processedConfigs = processedConfigs.map(({ data, context }) => ({ context, - data: filterByVisibility( + ...filterByVisibility( data, visibility, result.visibilityByPath, valueTransform, + withFilteredKeys, ), })); } else if (valueTransform) { processedConfigs = processedConfigs.map(({ data, context }) => ({ context, - data: filterByVisibility( + ...filterByVisibility( data, Array.from(CONFIG_VISIBILITIES), result.visibilityByPath, valueTransform, + withFilteredKeys, ), })); } diff --git a/packages/config-loader/src/lib/schema/types.ts b/packages/config-loader/src/lib/schema/types.ts index 30e47917bc..ef6a197f55 100644 --- a/packages/config-loader/src/lib/schema/types.ts +++ b/packages/config-loader/src/lib/schema/types.ts @@ -37,8 +37,10 @@ export const CONFIG_VISIBILITIES = ['frontend', 'backend', 'secret'] as const; /** * A type representing the possible configuration value visibilities + * + * @public */ -export type ConfigVisibility = typeof CONFIG_VISIBILITIES[number]; +export type ConfigVisibility = 'frontend' | 'backend' | 'secret'; /** * The default configuration visibility if no other values is given. @@ -73,6 +75,8 @@ export type ValidationFunc = (configs: AppConfig[]) => ValidationResult; /** * A function used to transform primitive configuration values. + * + * @public */ export type TransformFunc = ( value: T, @@ -81,8 +85,10 @@ export type TransformFunc = ( /** * Options used to process configuration data with a schema. + * + * @public */ -type ConfigProcessingOptions = { +export type ConfigSchemaProcessingOptions = { /** * The visibilities that should be included in the output data. * If omitted, the data will not be filtered by visibility. @@ -96,15 +102,24 @@ type ConfigProcessingOptions = { * will be omitted. */ valueTransform?: TransformFunc; + + /** + * Whether or not to include the `filteredKeys` property in the output `AppConfig`s. + * + * Default: `false`. + */ + withFilteredKeys?: boolean; }; /** * A loaded configuration schema that is ready to process configuration data. + * + * @public */ export type ConfigSchema = { process( appConfigs: AppConfig[], - options?: ConfigProcessingOptions, + options?: ConfigSchemaProcessingOptions, ): AppConfig[]; serialize(): JsonObject; diff --git a/packages/config-loader/src/lib/transform/include.test.ts b/packages/config-loader/src/lib/transform/include.test.ts index 44bf07ab4d..91df723621 100644 --- a/packages/config-loader/src/lib/transform/include.test.ts +++ b/packages/config-loader/src/lib/transform/include.test.ts @@ -24,9 +24,11 @@ const substituteMe = '${MY_SUBSTITUTION}'; const mySubstitution = 'fooSubstitution'; const env = jest.fn(async (name: string) => { - return ({ - SECRET: 'my-secret', - } as { [name: string]: string })[name]; + return ( + { + SECRET: 'my-secret', + } as { [name: string]: string } + )[name]; }); const substitute: TransformFunc = async value => { @@ -44,14 +46,16 @@ const substitute: TransformFunc = async value => { }; const readFile = jest.fn(async (path: string) => { - const content = ({ - [resolvePath(root, 'my-secret')]: 'secret', - [resolvePath(root, 'my-data.json')]: '{"a":{"b":{"c":42}}}', - [resolvePath(root, 'my-data.yaml')]: 'some:\n yaml:\n key: 7', - [resolvePath(root, 'my-data.yml')]: 'different: { key: hello }', - [resolvePath(root, 'invalid.yaml')]: 'foo: [}', - [resolvePath(root, `${mySubstitution}/my-data.json`)]: '{"foo":"bar"}', - } as { [key: string]: string })[path]; + const content = ( + { + [resolvePath(root, 'my-secret')]: 'secret', + [resolvePath(root, 'my-data.json')]: '{"a":{"b":{"c":42}}}', + [resolvePath(root, 'my-data.yaml')]: 'some:\n yaml:\n key: 7', + [resolvePath(root, 'my-data.yml')]: 'different: { key: hello }', + [resolvePath(root, 'invalid.yaml')]: 'foo: [}', + [resolvePath(root, `${mySubstitution}/my-data.json`)]: '{"foo":"bar"}', + } as { [key: string]: string } + )[path]; if (!content) { throw new Error('File not found!'); diff --git a/packages/config-loader/src/lib/transform/index.ts b/packages/config-loader/src/lib/transform/index.ts index 5053cd7443..eaa85cca3c 100644 --- a/packages/config-loader/src/lib/transform/index.ts +++ b/packages/config-loader/src/lib/transform/index.ts @@ -17,3 +17,4 @@ export { applyConfigTransforms } from './apply'; export { createIncludeTransform } from './include'; export { createSubstitutionTransform } from './substitution'; +export type { EnvFunc } from './types'; diff --git a/packages/config-loader/src/lib/transform/substitution.test.ts b/packages/config-loader/src/lib/transform/substitution.test.ts index dc6ed47597..b03c39c174 100644 --- a/packages/config-loader/src/lib/transform/substitution.test.ts +++ b/packages/config-loader/src/lib/transform/substitution.test.ts @@ -17,10 +17,12 @@ import { createSubstitutionTransform } from './substitution'; const env = jest.fn(async (name: string) => { - return ({ - SECRET: 'my-secret', - TOKEN: 'my-token', - } as { [name: string]: string })[name]; + return ( + { + SECRET: 'my-secret', + TOKEN: 'my-token', + } as { [name: string]: string } + )[name]; }); const substituteTransform = createSubstitutionTransform(env); diff --git a/packages/config-loader/src/lib/transform/types.ts b/packages/config-loader/src/lib/transform/types.ts index 20e5f88718..afd3550984 100644 --- a/packages/config-loader/src/lib/transform/types.ts +++ b/packages/config-loader/src/lib/transform/types.ts @@ -16,6 +16,7 @@ import { JsonValue } from '@backstage/config'; +/** @public */ export type EnvFunc = (name: string) => Promise; export type ReadFileFunc = (path: string) => Promise; diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index b0e236f9c9..3f51e76936 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; import mockFs from 'mock-fs'; +import fs from 'fs-extra'; describe('loadConfig', () => { - beforeAll(() => { + beforeEach(() => { process.env.MY_SECRET = 'is-secret'; process.env.SUBSTITUTE_ME = 'substituted'; @@ -63,7 +65,7 @@ describe('loadConfig', () => { }); }); - afterAll(() => { + afterEach(() => { mockFs.restore(); }); @@ -170,4 +172,81 @@ describe('loadConfig', () => { }, ]); }); + + it('watches config files', async () => { + const onChange = defer(); + const stopSignal = defer(); + + await expect( + loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: onChange.resolve, + stopSignal: stopSignal.promise, + }, + }), + ).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await expect(onChange.promise).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'New Title', + }, + }, + }, + ]); + + stopSignal.resolve(); + }); + + it('stops watching config files', async () => { + const stopSignal = defer(); + + await loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: () => { + expect('not').toBe('called'); + }, + stopSignal: stopSignal.promise, + }, + }); + + stopSignal.resolve(); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + + function defer() { + let resolve: (value: T) => void; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve: resolve! }; + } }); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 3846d6474b..315b44c4ee 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import yaml from 'yaml'; +import chokidar from 'chokidar'; import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path'; import { AppConfig } from '@backstage/config'; import { @@ -26,6 +27,7 @@ import { } from './lib'; import { EnvFunc } from './lib/transform/types'; +/** @public */ export type LoadConfigOptions = { // The root directory of the config loading context. Used to find default configs. configRoot: string; @@ -42,13 +44,32 @@ export type LoadConfigOptions = { * @experimental This API is not stable and may change at any point */ experimentalEnvFunc?: EnvFunc; + + /** + * An optional configuration that enables watching of config files. + */ + watch?: { + /** + * A listener that is called when a config file is changed. + */ + onChange: (configs: AppConfig[]) => void; + + /** + * An optional signal that stops the watcher once the promise resolves. + */ + stopSignal?: Promise; + }; }; +/** + * Load configuration data. + * + * @public + */ export async function loadConfig( options: LoadConfigOptions, ): Promise { - const configs = []; - const { configRoot, experimentalEnvFunc: envFunc } = options; + const { configRoot, experimentalEnvFunc: envFunc, watch } = options; const configPaths = options.configPaths.slice(); // If no paths are provided, we default to reading @@ -64,7 +85,9 @@ export async function loadConfig( const env = envFunc ?? (async (name: string) => process.env[name]); - try { + const loadConfigFiles = async () => { + const configs = []; + for (const configPath of configPaths) { if (!isAbsolute(configPath)) { throw new Error(`Config load path is not absolute: '${configPath}'`); @@ -83,13 +106,50 @@ export async function loadConfig( configs.push({ data, context: basename(configPath) }); } + + return configs; + }; + + let fileConfigs; + try { + fileConfigs = await loadConfigFiles(); } catch (error) { throw new Error( `Failed to read static configuration file, ${error.message}`, ); } - configs.push(...readEnvConfig(process.env)); + const envConfigs = await readEnvConfig(process.env); - return configs; + // Set up config file watching if requested by the caller + if (watch) { + let currentSerializedConfig = JSON.stringify(fileConfigs); + + const watcher = chokidar.watch(configPaths, { + usePolling: process.env.NODE_ENV === 'test', + }); + watcher.on('change', async () => { + try { + const newConfigs = await loadConfigFiles(); + const newSerializedConfig = JSON.stringify(newConfigs); + + if (currentSerializedConfig === newSerializedConfig) { + return; + } + currentSerializedConfig = newSerializedConfig; + + watch.onChange([...newConfigs, ...envConfigs]); + } catch (error) { + console.error(`Failed to reload configuration files, ${error}`); + } + }); + + if (watch.stopSignal) { + watch.stopSignal.then(() => { + watcher.close(); + }); + } + } + + return [...fileConfigs, ...envConfigs]; } diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 5cababf94e..7691ed2865 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/config +## 0.1.9 + +### Patch Changes + +- f88b2c7db: Documented `Config` interface and mark types as public. + +## 0.1.8 + +### Patch Changes + +- 47113f1f1: Only warn once per key when trying to read visibility-filtered values + +## 0.1.7 + +### Patch Changes + +- 90f25476a: Extended the `Config` interface to have an optional `subscribe` method that can be used be notified of updates to the configuration. + +## 0.1.6 + +### Patch Changes + +- e9d3983ee: Add warning when trying to access configuration values that have been filtered out by visibility. + ## 0.1.5 ### Patch Changes diff --git a/packages/config/api-report.md b/packages/config/api-report.md index 0c243aab48..068498b17a 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -3,14 +3,18 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -// @public (undocumented) +// @public export type AppConfig = { context: string; data: JsonObject; + filteredKeys?: string[]; }; -// @public (undocumented) +// @public export type Config = { + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; has(key: string): boolean; keys(): string[]; get(key?: string): T; @@ -29,7 +33,7 @@ export type Config = { getOptionalStringArray(key: string): string[] | undefined; }; -// @public (undocumented) +// @public export class ConfigReader implements Config { constructor( data: JsonObject | undefined, @@ -73,18 +77,18 @@ export class ConfigReader implements Config { keys(): string[]; } -// @public (undocumented) +// @public export interface JsonArray extends Array {} -// @public (undocumented) +// @public export type JsonObject = { [key in string]?: JsonValue; }; -// @public (undocumented) +// @public export type JsonPrimitive = number | string | boolean | null; -// @public (undocumented) +// @public export type JsonValue = JsonObject | JsonArray | JsonPrimitive; // (No @packageDocumentation comment for this package) diff --git a/packages/config/package.json b/packages/config/package.json index 400e4548cd..969bc44045 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.5", + "version": "0.1.9", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index b1ba536797..7f3513abf6 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { withLogCollector } from '../../test-utils-core/src'; import { ConfigReader } from './reader'; const DATA = { @@ -63,9 +64,9 @@ function expectValidValues(config: ConfigReader) { strings: ['string1', 'string2'], }); expect(config.getConfig('nested').getString('string')).toBe('string'); - expect( - config.getOptionalConfig('nested')!.getStringArray('strings'), - ).toEqual(['string1', 'string2']); + expect(config.getOptionalConfig('nested')!.getStringArray('strings')).toEqual( + ['string1', 'string2'], + ); expect(config.getOptional('missing')).toBe(undefined); expect(config.getOptionalConfig('missing')).toBe(undefined); expect(config.getOptionalConfigArray('missing')).toBe(undefined); @@ -185,6 +186,85 @@ describe('ConfigReader', () => { const config = new ConfigReader(DATA, CTX); expectInvalidValues(config); }); + + it('should warn when accessing filtered keys in development mode', () => { + const oldEnv = process.env.NODE_ENV; + (process.env as any).NODE_ENV = 'development'; + + const config = ConfigReader.fromConfigs([ + { + data: DATA, + context: CTX, + filteredKeys: ['a', 'a2', 'b[0]'], + }, + ]); + + expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({ + warn: [ + "Failed to read configuration value at 'a' as it is not visible. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.", + ], + }); + expect( + withLogCollector(() => config.getOptionalString('a2')), + ).toMatchObject({ + warn: [ + "Failed to read configuration value at 'a2' as it is not visible. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.", + ], + }); + expect( + withLogCollector(() => config.getOptionalConfigArray('b')), + ).toMatchObject({ + warn: [ + "Failed to read configuration array at 'b' as it does not have any visible elements. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.", + ], + }); + + (process.env as any).NODE_ENV = oldEnv; + }); + + it('only warns once when accessing filtered keys in development mode', () => { + const oldEnv = process.env.NODE_ENV; + (process.env as any).NODE_ENV = 'development'; + + const config = ConfigReader.fromConfigs([ + { + data: DATA, + context: CTX, + filteredKeys: ['a'], + }, + ]); + + expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({ + warn: [ + "Failed to read configuration value at 'a' as it is not visible. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.", + ], + }); + expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({ + warn: [], + }); + + (process.env as any).NODE_ENV = oldEnv; + }); + + it('should not warn when accessing filtered keys outside of development mode', () => { + const config = ConfigReader.fromConfigs([ + { + data: DATA, + context: CTX, + filteredKeys: ['a', 'b[0]'], + }, + ]); + + expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({ + warn: [], + }); + expect(withLogCollector(() => config.getOptionalString('a'))).toMatchObject( + { warn: [] }, + ); + expect( + withLogCollector(() => config.getOptionalConfigArray('b')), + ).toMatchObject({ warn: [] }); + }); }); describe('ConfigReader with fallback', () => { diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 11346d4a50..92ca0eede3 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -54,7 +54,23 @@ const errors = { }, }; +/** + * An implementation of the `Config` interface that uses a plain JavaScript object + * for the backing data, with the ability of linking multiple readers together. + * + * @public + */ export class ConfigReader implements Config { + /** + * A set of key paths that where removed from the config due to not being visible. + * + * This was added as a mutable private member to avoid changes to the public API. + * Its only purpose of this is to warn users of missing visibility when running + * the frontend in development mode. + */ + private filteredKeys?: string[]; + private notifiedFilteredKeys = new Set(); + static fromConfigs(configs: AppConfig[]): ConfigReader { if (configs.length === 0) { return new ConfigReader(undefined); @@ -62,9 +78,14 @@ export class ConfigReader implements Config { // Merge together all configs into a single config with recursive fallback // readers, giving the first config object in the array the lowest priority. - return configs.reduce((previousReader, { data, context }) => { - return new ConfigReader(data, context, previousReader); - }, undefined!); + return configs.reduce( + (previousReader, { data, context, filteredKeys }) => { + const reader = new ConfigReader(data, context, previousReader); + reader.filteredKeys = filteredKeys; + return reader; + }, + undefined!, + ); } constructor( @@ -101,6 +122,22 @@ export class ConfigReader implements Config { const fallbackValue = this.fallback?.getOptional(key); if (value === undefined) { + if (process.env.NODE_ENV === 'development') { + if (fallbackValue === undefined && key) { + const fullKey = this.fullKey(key); + if ( + this.filteredKeys?.includes(fullKey) && + !this.notifiedFilteredKeys.has(fullKey) + ) { + this.notifiedFilteredKeys.add(fullKey); + // eslint-disable-next-line no-console + console.warn( + `Failed to read configuration value at '${fullKey}' as it is not visible. ` + + 'See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.', + ); + } + } + } return fallbackValue; } else if (fallbackValue === undefined) { return value as T; @@ -127,10 +164,9 @@ export class ConfigReader implements Config { getOptionalConfig(key: string): ConfigReader | undefined { const value = this.readValue(key); const fallbackConfig = this.fallback?.getOptionalConfig(key); - const prefix = this.fullKey(key); if (isObject(value)) { - return new ConfigReader(value, this.context, fallbackConfig, prefix); + return this.copy(value, key, fallbackConfig); } if (value !== undefined) { throw new TypeError( @@ -163,18 +199,24 @@ export class ConfigReader implements Config { }); if (!configs) { + if (process.env.NODE_ENV === 'development') { + const fullKey = this.fullKey(key); + if ( + this.filteredKeys?.some(k => k.startsWith(fullKey)) && + !this.notifiedFilteredKeys.has(key) + ) { + this.notifiedFilteredKeys.add(key); + // eslint-disable-next-line no-console + console.warn( + `Failed to read configuration array at '${key}' as it does not have any visible elements. ` + + 'See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.', + ); + } + } return undefined; } - return configs.map( - (obj, index) => - new ConfigReader( - obj, - this.context, - undefined, - this.fullKey(`${key}[${index}]`), - ), - ); + return configs.map((obj, index) => this.copy(obj, `${key}[${index}]`)); } getNumber(key: string): number { @@ -261,6 +303,17 @@ export class ConfigReader implements Config { return `${this.prefix}${this.prefix ? '.' : ''}${key}`; } + private copy(data: JsonObject, key: string, fallback?: ConfigReader) { + const reader = new ConfigReader( + data, + this.context, + fallback, + this.fullKey(key), + ); + reader.filteredKeys = this.filteredKeys; + return reader; + } + private readConfigValue( key: string, validate: ( @@ -270,6 +323,21 @@ export class ConfigReader implements Config { const value = this.readValue(key); if (value === undefined) { + if (process.env.NODE_ENV === 'development') { + const fullKey = this.fullKey(key); + if ( + this.filteredKeys?.includes(fullKey) && + !this.notifiedFilteredKeys.has(fullKey) + ) { + this.notifiedFilteredKeys.add(fullKey); + // eslint-disable-next-line no-console + console.warn( + `Failed to read configuration value at '${fullKey}' as it is not visible. ` + + 'See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.', + ); + } + } + return this.fallback?.readConfigValue(key, validate); } const result = validate(value); diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index d8f12f4f6e..d5f551cd69 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -14,39 +14,157 @@ * limitations under the License. */ +/** + * A type representing all allowed JSON primitive values. + * + * @public + */ export type JsonPrimitive = number | string | boolean | null; + +/** + * A type representing all allowed JSON object values. + * + * @public + */ export type JsonObject = { [key in string]?: JsonValue }; + +/** + * A type representing all allowed JSON array values. + * + * @public + */ export interface JsonArray extends Array {} + +/** + * A type representing all allowed JSON values. + * + * @public + */ export type JsonValue = JsonObject | JsonArray | JsonPrimitive; +/** + * A serialized form of configuration data that carries additional context. + * + * @public + */ export type AppConfig = { + /** + * A string representing the source of this configuration data, for example a filepath. + */ context: string; + /** + * The configuration data itself. + */ data: JsonObject; + /** + * A list of keys that where filtered out from the configuration when it was loaded. + * + * This can be used to warn the user if they try to read any of these keys. + */ + filteredKeys?: string[]; }; +/** + * The interface used to represent static configuration at runtime. + * + * @public + */ export type Config = { + /** + * Subscribes to the configuration object in order to receive a notification + * whenever any value within the configuration has changed. + * + * This method is optional to implement, and consumers need to check if it is + * implemented before invoking it. + */ + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; + + /** + * Checks whether the given key is present. + */ has(key: string): boolean; + /** + * Lists all available configuration keys. + */ keys(): string[]; + /** + * Same as `getOptional`, but will throw an error if there's no value for the given key. + */ get(key?: string): T; + + /** + * Read out all configuration data for the given key. + * + * Usage of this method should be avoided as the typed alternatives provide + * much better error reporting. The main use-case of this method is to determine + * the type of a configuration value in the case where there are multiple possible + * shapes of the configuration. + */ getOptional(key?: string): T | undefined; + /** + * Same as `getOptionalConfig`, but will throw an error if there's no value for the given key. + */ getConfig(key: string): Config; + + /** + * Creates a sub-view of the configuration object. + * The configuration value at the position of the provided key must be an object. + */ getOptionalConfig(key: string): Config | undefined; + /** + * Same as `getOptionalConfigArray`, but will throw an error if there's no value for the given key. + */ getConfigArray(key: string): Config[]; + + /** + * Creates a sub-view of an array of configuration objects. + * The configuration value at the position of the provided key must be an array of objects. + */ getOptionalConfigArray(key: string): Config[] | undefined; + /** + * Same as `getOptionalNumber`, but will throw an error if there's no value for the given key. + */ getNumber(key: string): number; + + /** + * Reads a configuration value at the given key, expecting it to be a number. + */ getOptionalNumber(key: string): number | undefined; + /** + * Same as `getOptionalBoolean`, but will throw an error if there's no value for the given key. + */ getBoolean(key: string): boolean; + + /** + * Reads a configuration value at the given key, expecting it to be a boolean. + */ getOptionalBoolean(key: string): boolean | undefined; + /** + * Same as `getOptionalString`, but will throw an error if there's no value for the given key. + */ getString(key: string): string; + + /** + * Reads a configuration value at the given key, expecting it to be a string. + */ getOptionalString(key: string): string | undefined; + /** + * Same as `getOptionalStringArray`, but will throw an error if there's no value for the given key. + */ getStringArray(key: string): string[]; + + /** + * Reads a configuration value at the given key, expecting it to be an array of strings. + */ getOptionalStringArray(key: string): string[] | undefined; }; diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 4f09f231c3..9020661647 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,89 @@ # @backstage/core-app-api +## 0.1.12 + +### Patch Changes + +- 841666a19: Removed deprecated internal functions. +- Updated dependencies + - @backstage/core-components@0.4.1 + - @backstage/config@0.1.9 + - @backstage/core-plugin-api@0.1.7 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.0 + +## 0.1.10 + +### Patch Changes + +- cfcb486aa: Add system icons for the built-in entity types and use them in the entity list of the `catalog-import` plugin. +- 392b36fa1: Added support for using authenticating via GitHub Apps in addition to GitHub OAuth Apps. It used to be possible to use GitHub Apps, but they did not handle session refresh correctly. + + Note that GitHub Apps handle OAuth scope at the app installation level, meaning that the `scope` parameter for `getAccessToken` has no effect. When calling `getAccessToken` in open source plugins, one should still include the appropriate scope, but also document in the plugin README what scopes are required in the case of GitHub Apps. + + In addition, the `authHandler` and `signInResolver` options have been implemented for the GitHub provider in the auth backend. + +- Updated dependencies + - @backstage/core-components@0.3.3 + - @backstage/config@0.1.8 + +## 0.1.9 + +### Patch Changes + +- 72a31c29a: Add support for additional app origins +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + +## 0.1.8 + +### Patch Changes + +- 362657623: Add support for serving the app with a base path other than `/`, which is enabled by including the path in `app.baseUrl`. +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.3.0 + - @backstage/config@0.1.6 + - @backstage/core-plugin-api@0.1.5 + +## 0.1.6 + +### Patch Changes + +- 9d40fcb1e: - Bumping `material-ui/core` version to at least `4.12.2` as they made some breaking changes in later versions which broke `Pagination` of the `Table`. + - Switching out `material-table` to `@material-table/core` for support for the later versions of `material-ui/core` + - This causes a minor API change to `@backstage/core-components` as the interface for `Table` re-exports the `prop` from the underlying `Table` components. + - `onChangeRowsPerPage` has been renamed to `onRowsPerPageChange` + - `onChangePage` has been renamed to `onPageChange` + - Migration guide is here: https://material-table-core.com/docs/breaking-changes +- Updated dependencies + - @backstage/core-components@0.2.0 + - @backstage/core-plugin-api@0.1.4 + - @backstage/theme@0.2.9 + +## 0.1.5 + +### Patch Changes + +- ea249c6e6: Fix a bug in `FlatRoutes` that prevented outlets from working with the root route, as well as matching root routes too broadly. +- Updated dependencies + - @backstage/core-components@0.1.6 + ## 0.1.4 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index af86f042a0..20d8a5a770 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -54,6 +54,8 @@ import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueChange } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; +// Warning: (ae-missing-release-tag) "AlertApiForwarder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class AlertApiForwarder implements AlertApi { // (undocumented) @@ -62,11 +64,11 @@ export class AlertApiForwarder implements AlertApi { post(alert: AlertMessage): void; } +// Warning: (ae-missing-release-tag) "ApiFactoryHolder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type ApiFactoryHolder = { - get( - api: ApiRef, - ): + get(api: ApiRef): | ApiFactory< T, T, @@ -77,12 +79,12 @@ export type ApiFactoryHolder = { | undefined; }; +// Warning: (ae-missing-release-tag) "ApiFactoryRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class ApiFactoryRegistry implements ApiFactoryHolder { // (undocumented) - get( - api: ApiRef, - ): + get(api: ApiRef): | ApiFactory< T, T, @@ -93,15 +95,18 @@ export class ApiFactoryRegistry implements ApiFactoryHolder { | undefined; // (undocumented) getAllApis(): Set; + // Warning: (ae-forgotten-export) The symbol "ApiFactoryScope" needs to be exported by the entry point index.d.ts register< Api, Impl extends Api, Deps extends { [name in string]: unknown; - } + }, >(scope: ApiFactoryScope, factory: ApiFactory): boolean; } +// Warning: (ae-missing-release-tag) "ApiProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const ApiProvider: { ({ apis, children }: PropsWithChildren): JSX.Element; @@ -115,19 +120,31 @@ export const ApiProvider: { }; }; +// Warning: (ae-missing-release-tag) "ApiRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class ApiRegistry implements ApiHolder { constructor(apis: Map); + // Warning: (ae-forgotten-export) The symbol "ApiRegistryBuilder" needs to be exported by the entry point index.d.ts + // // (undocumented) static builder(): ApiRegistryBuilder; + // Warning: (ae-forgotten-export) The symbol "ApiImpl" needs to be exported by the entry point index.d.ts + // // (undocumented) static from(apis: ApiImpl[]): ApiRegistry; // (undocumented) get(api: ApiRef): T | undefined; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static with(api: ApiRef, impl: T): ApiRegistry; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen with(api: ApiRef, impl: T): ApiRegistry; } +// Warning: (ae-missing-release-tag) "ApiResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class ApiResolver implements ApiHolder { constructor(factories: ApiFactoryHolder); @@ -139,6 +156,8 @@ export class ApiResolver implements ApiHolder { ): void; } +// Warning: (ae-missing-release-tag) "AppComponents" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; @@ -149,9 +168,13 @@ export type AppComponents = { SignInPage?: ComponentType; }; +// Warning: (ae-missing-release-tag) "AppConfigLoader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export type AppConfigLoader = () => Promise; +// Warning: (ae-missing-release-tag) "AppContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type AppContext = { getPlugins(): BackstagePlugin[]; @@ -159,6 +182,8 @@ export type AppContext = { getComponents(): AppComponents; }; +// Warning: (ae-missing-release-tag) "AppOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type AppOptions = { apis?: Iterable; @@ -173,11 +198,16 @@ export type AppOptions = { bindRoutes?(context: { bind: AppRouteBinder }): void; }; +// Warning: (ae-forgotten-export) The symbol "PartialKeys" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "TargetRouteMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "KeysWithType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "AppRouteBinder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type AppRouteBinder = < ExternalRoutes extends { [name: string]: ExternalRouteRef; - } + }, >( externalRoutes: ExternalRoutes, targetRoutes: PartialKeys< @@ -186,6 +216,8 @@ export type AppRouteBinder = < >, ) => void; +// Warning: (ae-missing-release-tag) "AppThemeSelector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class AppThemeSelector implements AppThemeApi { constructor(themes: AppTheme[]); @@ -201,8 +233,12 @@ export class AppThemeSelector implements AppThemeApi { setActiveThemeId(themeId?: string): void; } +// Warning: (ae-missing-release-tag) "Auth0Auth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class Auth0Auth { + // Warning: (ae-forgotten-export) The symbol "OAuthApiCreateOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) static create({ discoveryApi, @@ -213,6 +249,8 @@ export class Auth0Auth { }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; } +// Warning: (ae-missing-release-tag) "BackstageApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type BackstageApp = { getPlugins(): BackstagePlugin[]; @@ -221,6 +259,8 @@ export type BackstageApp = { getRouter(): ComponentType<{}>; }; +// Warning: (ae-missing-release-tag) "BackstagePluginWithAnyOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type BackstagePluginWithAnyOutput = Omit< BackstagePlugin, @@ -229,6 +269,8 @@ export type BackstagePluginWithAnyOutput = Omit< output(): (PluginOutput | UnknownPluginOutput)[]; }; +// Warning: (ae-missing-release-tag) "BootErrorPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type BootErrorPageProps = { step: 'load-config' | 'load-chunk'; @@ -237,12 +279,19 @@ export type BootErrorPageProps = { export { ConfigReader }; +// Warning: (ae-forgotten-export) The symbol "PrivateAppImpl" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "createApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export function createApp(options?: AppOptions): PrivateAppImpl; +// Warning: (ae-missing-release-tag) "defaultConfigLoader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export const defaultConfigLoader: AppConfigLoader; +// Warning: (ae-missing-release-tag) "ErrorAlerter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class ErrorAlerter implements ErrorApi { constructor(alertApi: AlertApi, errorApi: ErrorApi); @@ -259,6 +308,8 @@ export class ErrorAlerter implements ErrorApi { post(error: Error, context?: ErrorContext): void; } +// Warning: (ae-missing-release-tag) "ErrorApiForwarder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class ErrorApiForwarder implements ErrorApi { // (undocumented) @@ -270,6 +321,8 @@ export class ErrorApiForwarder implements ErrorApi { post(error: Error, context?: ErrorContext): void; } +// Warning: (ae-missing-release-tag) "ErrorBoundaryFallbackProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type ErrorBoundaryFallbackProps = { plugin?: BackstagePlugin; @@ -277,9 +330,13 @@ export type ErrorBoundaryFallbackProps = { resetError: () => void; }; +// Warning: (ae-missing-release-tag) "FeatureFlagged" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element; +// Warning: (ae-missing-release-tag) "FeatureFlaggedProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type FeatureFlaggedProps = { children: ReactNode; @@ -292,11 +349,17 @@ export type FeatureFlaggedProps = { } ); +// Warning: (ae-forgotten-export) The symbol "FlatRoutesProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "FlatRoutes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null; +// Warning: (ae-missing-release-tag) "GithubAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class GithubAuth implements OAuthApi, SessionApi { + // Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts constructor(sessionManager: SessionManager); // (undocumented) static create({ @@ -324,17 +387,21 @@ export class GithubAuth implements OAuthApi, SessionApi { signOut(): Promise; } +// Warning: (ae-missing-release-tag) "GithubSession" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type GithubSession = { providerInfo: { accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; backstageIdentity: BackstageIdentity; }; +// Warning: (ae-missing-release-tag) "GitlabAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class GitlabAuth { // (undocumented) @@ -347,6 +414,8 @@ export class GitlabAuth { }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T; } +// Warning: (ae-missing-release-tag) "GoogleAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class GoogleAuth { // (undocumented) @@ -359,6 +428,8 @@ export class GoogleAuth { }: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } +// Warning: (ae-missing-release-tag) "LocalStorageFeatureFlags" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class LocalStorageFeatureFlags implements FeatureFlagsApi { // (undocumented) @@ -371,6 +442,8 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { save(options: FeatureFlagsSaveOptions): void; } +// Warning: (ae-missing-release-tag) "MicrosoftAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class MicrosoftAuth { // (undocumented) @@ -383,6 +456,8 @@ export class MicrosoftAuth { }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; } +// Warning: (ae-missing-release-tag) "OAuth2" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class OAuth2 implements @@ -390,8 +465,12 @@ export class OAuth2 OpenIdConnectApi, ProfileInfoApi, BackstageIdentityApi, - SessionApi { + SessionApi +{ + // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts constructor(options: Options); + // Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) static create({ discoveryApi, @@ -422,6 +501,8 @@ export class OAuth2 signOut(): Promise; } +// Warning: (ae-missing-release-tag) "OAuth2Session" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type OAuth2Session = { providerInfo: { @@ -434,6 +515,8 @@ export type OAuth2Session = { backstageIdentity: BackstageIdentity; }; +// Warning: (ae-missing-release-tag) "OAuthRequestManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class OAuthRequestManager implements OAuthRequestApi { // (undocumented) @@ -442,6 +525,8 @@ export class OAuthRequestManager implements OAuthRequestApi { createAuthRequester(options: AuthRequesterOptions): AuthRequester; } +// Warning: (ae-missing-release-tag) "OktaAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class OktaAuth { // (undocumented) @@ -454,8 +539,12 @@ export class OktaAuth { }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T; } +// Warning: (ae-missing-release-tag) "OneLoginAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class OneLoginAuth { + // Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) static create({ discoveryApi, @@ -465,10 +554,16 @@ export class OneLoginAuth { }: CreateOptions_2): typeof oneloginAuthApiRef.T; } +// Warning: (ae-missing-release-tag) "SamlAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class SamlAuth - implements ProfileInfoApi, BackstageIdentityApi, SessionApi { + implements ProfileInfoApi, BackstageIdentityApi, SessionApi +{ + // Warning: (ae-forgotten-export) The symbol "SamlSession" needs to be exported by the entry point index.d.ts constructor(sessionManager: SessionManager); + // Warning: (ae-forgotten-export) The symbol "AuthApiCreateOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) static create({ discoveryApi, @@ -489,11 +584,15 @@ export class SamlAuth signOut(): Promise; } +// Warning: (ae-missing-release-tag) "SignInPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SignInPageProps = { onResult(result: SignInResult): void; }; +// Warning: (ae-missing-release-tag) "SignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SignInResult = { userId: string; @@ -502,11 +601,15 @@ export type SignInResult = { signOut?: () => Promise; }; +// Warning: (ae-missing-release-tag) "UnhandledErrorForwarder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class UnhandledErrorForwarder { static forward(errorApi: ErrorApi, errorContext: ErrorContext): void; } +// Warning: (ae-missing-release-tag) "UrlPatternDiscovery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public export class UrlPatternDiscovery implements DiscoveryApi { static compile(pattern: string): UrlPatternDiscovery; @@ -514,9 +617,13 @@ export class UrlPatternDiscovery implements DiscoveryApi { getBaseUrl(pluginId: string): Promise; } +// Warning: (ae-missing-release-tag) "WebStorage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export class WebStorage implements StorageApi { constructor(namespace: string, errorApi: ErrorApi); + // Warning: (ae-forgotten-export) The symbol "CreateStorageApiOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) static create(options: CreateStorageApiOptions): WebStorage; // (undocumented) @@ -531,5 +638,11 @@ export class WebStorage implements StorageApi { set(key: string, data: T): Promise; } +// Warnings were encountered during analysis: +// +// src/apis/system/ApiProvider.d.ts:9:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts +// src/app/types.d.ts:88:5 - (ae-forgotten-export) The symbol "UnknownPluginOutput" needs to be exported by the entry point index.d.ts +// src/app/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 0f6773497b..790b1e29fb 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.4", + "version": "0.1.12", "private": false, "publishConfig": { "access": "public", @@ -29,13 +29,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.1.4", - "@backstage/config": "^0.1.3", - "@backstage/core-plugin-api": "^0.1.3", - "@backstage/theme": "^0.2.8", - "@material-ui/core": "^4.11.0", + "@backstage/core-components": "^0.4.1", + "@backstage/config": "^0.1.9", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/theme": "^0.2.10", + "@backstage/version-bridge": "^0.1.0", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@types/react": "^16.9", + "@types/react": "*", "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", "react": "^16.12.0", @@ -44,9 +45,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.3", - "@backstage/test-utils": "^0.1.14", - "@backstage/test-utils-core": "^0.1.1", + "@backstage/cli": "^0.7.11", + "@backstage/test-utils": "^0.1.17", + "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx b/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx index afc25b2e8c..1193592e96 100644 --- a/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx +++ b/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx @@ -194,8 +194,7 @@ describe('FeatureFlags', () => { it('throws an error if length is greater than 150 characters', () => { expect(() => featureFlags.registerFlag({ - name: - 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat', + name: 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat', pluginId: 'plugin-three', }), ).toThrow(/not exceed 150 characters/i); diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 21ebc5699a..85e3e79ad5 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -29,15 +29,17 @@ import { import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { AuthSessionStore, + RefreshingAuthSessionManager, StaticAuthSessionManager, } from '../../../../lib/AuthSessionManager'; import { OAuthApiCreateOptions } from '../types'; +import { OptionalRefreshSessionManagerMux } from '../../../../lib/AuthSessionManager/OptionalRefreshSessionManagerMux'; export type GithubAuthResponse = { providerInfo: { accessToken: string; scope: string; - expiresInSeconds: number; + expiresInSeconds?: number; }; profile: ProfileInfo; backstageIdentity: BackstageIdentity; @@ -68,27 +70,46 @@ class GithubAuth implements OAuthApi, SessionApi { providerInfo: { accessToken: res.providerInfo.accessToken, scopes: GithubAuth.normalizeScope(res.providerInfo.scope), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), + expiresAt: res.providerInfo.expiresInSeconds + ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000) + : undefined, }, }; }, }); - const sessionManager = new StaticAuthSessionManager({ + const refreshingSessionManager = new RefreshingAuthSessionManager({ connector, defaultScopes: new Set(defaultScopes), sessionScopes: (session: GithubSession) => session.providerInfo.scopes, + sessionShouldRefresh: (session: GithubSession) => { + const { expiresAt } = session.providerInfo; + if (!expiresAt) { + return false; + } + const expiresInSec = (expiresAt.getTime() - Date.now()) / 1000; + return expiresInSec < 60 * 5; + }, }); - const authSessionStore = new AuthSessionStore({ - manager: sessionManager, + const staticSessionManager = new AuthSessionStore({ + manager: new StaticAuthSessionManager({ + connector, + defaultScopes: new Set(defaultScopes), + sessionScopes: (session: GithubSession) => session.providerInfo.scopes, + }), storageKey: `${provider.id}Session`, sessionScopes: (session: GithubSession) => session.providerInfo.scopes, }); - return new GithubAuth(authSessionStore); + const sessionManagerMux = new OptionalRefreshSessionManagerMux({ + refreshingSessionManager, + staticSessionManager, + sessionCanRefresh: session => + session.providerInfo.expiresAt !== undefined, + }); + + return new GithubAuth(sessionManagerMux); } constructor(private readonly sessionManager: SessionManager) {} diff --git a/packages/core-app-api/src/apis/implementations/auth/github/types.ts b/packages/core-app-api/src/apis/implementations/auth/github/types.ts index 88df25b49d..f5dae3a064 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/types.ts @@ -20,7 +20,7 @@ export type GithubSession = { providerInfo: { accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; backstageIdentity: BackstageIdentity; diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 8c27e0aa63..24db8d2560 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -65,7 +65,8 @@ class OAuth2 OpenIdConnectApi, ProfileInfoApi, BackstageIdentityApi, - SessionApi { + SessionApi +{ static create({ discoveryApi, environment = 'development', diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index 5afb39a2ec..d1793f890e 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { Context, useContext } from 'react'; +import React from 'react'; import { useApi, createApiRef, @@ -26,8 +26,7 @@ import { ApiProvider } from './ApiProvider'; import { ApiRegistry } from './ApiRegistry'; import { render } from '@testing-library/react'; import { withLogCollector } from '@backstage/test-utils-core'; -import { getGlobalSingleton } from '../../lib/globalObject'; -import { VersionedValue } from '../../lib/versionedValues'; +import { useVersionedContext } from '@backstage/version-bridge'; describe('ApiProvider', () => { type Api = () => string; @@ -116,11 +115,11 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow(/^No provider available for api-context context/); + }).toThrow(/^API context is not available/); }).error, ).toEqual([ expect.stringMatching( - /^Error: Uncaught \[Error: No provider available for api-context context/, + /^Error: Uncaught \[Error: API context is not available/, ), expect.stringMatching( /^The above error occurred in the component/, @@ -131,11 +130,11 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow(/^No provider available for api-context context/); + }).toThrow(/^API context is not available/); }).error, ).toEqual([ expect.stringMatching( - /^Error: Uncaught \[Error: No provider available for api-context context/, + /^Error: Uncaught \[Error: API context is not available/, ), expect.stringMatching( /^The above error occurred in the component/, @@ -185,12 +184,10 @@ describe('ApiProvider', () => { }); describe('v1 consumer', () => { - const ApiContext = getGlobalSingleton< - Context> - >('api-context'); - function useMockApiV1(apiRef: ApiRef): T { - const impl = useContext(ApiContext)?.atVersion(1)?.get(apiRef); + const impl = useVersionedContext<{ 1: ApiHolder }>('api-context') + ?.atVersion(1) + ?.get(apiRef); if (!impl) { throw new Error('no impl'); } diff --git a/packages/core-app-api/src/apis/system/ApiProvider.tsx b/packages/core-app-api/src/apis/system/ApiProvider.tsx index ce6c388087..aa57fa9310 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.tsx @@ -14,30 +14,21 @@ * limitations under the License. */ -import React, { - createContext, - useContext, - ReactNode, - PropsWithChildren, -} from 'react'; +import React, { useContext, ReactNode, PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiAggregator } from './ApiAggregator'; -import { getOrCreateGlobalSingleton } from '../../lib/globalObject'; import { - VersionedValue, createVersionedValueMap, -} from '../../lib/versionedValues'; + createVersionedContext, +} from '@backstage/version-bridge'; type ApiProviderProps = { apis: ApiHolder; children: ReactNode; }; -type ApiContextType = VersionedValue<{ 1: ApiHolder }> | undefined; -const ApiContext = getOrCreateGlobalSingleton('api-context', () => - createContext(undefined), -); +const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context'); export const ApiProvider = ({ apis, diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx index a95554709a..c8bd8fec8f 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/App.tsx @@ -107,6 +107,20 @@ export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { return result; } +/** + * Get the app base path from the configured app baseUrl. + * + * The returned path does not have a trailing slash. + */ +function getBasePath(configApi: Config) { + let { pathname } = new URL( + configApi.getOptionalString('app.baseUrl') ?? '/', + 'http://dummy.dev', // baseUrl can be specified as just a path + ); + pathname = pathname.replace(/\/*$/, ''); + return pathname; +} + type FullAppOptions = { apis: Iterable; icons: NonNullable; @@ -216,37 +230,33 @@ export class PrivateAppImpl implements BackstageApp { [], ); - const { - routePaths, - routeParents, - routeObjects, - featureFlags, - } = useMemo(() => { - const result = traverseElementTree({ - root: children, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - collectedPlugins: pluginCollector, - featureFlags: featureFlagCollector, - }, - }); + const { routePaths, routeParents, routeObjects, featureFlags } = + useMemo(() => { + const result = traverseElementTree({ + root: children, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routePaths: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + collectedPlugins: pluginCollector, + featureFlags: featureFlagCollector, + }, + }); - validateRoutes(result.routePaths, result.routeParents); + validateRoutes(result.routePaths, result.routeParents); - // TODO(Rugvip): Restructure the public API so that we can get an immediate view of - // the app, rather than having to wait for the provider to render. - // For now we need to push the additional plugins we find during - // collection and then make sure we initialize things afterwards. - result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); - this.verifyPlugins(this.plugins); + // TODO(Rugvip): Restructure the public API so that we can get an immediate view of + // the app, rather than having to wait for the provider to render. + // For now we need to push the additional plugins we find during + // collection and then make sure we initialize things afterwards. + result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); + this.verifyPlugins(this.plugins); - // Initialize APIs once all plugins are available - this.getApiHolder(); - return result; - }, [children]); + // Initialize APIs once all plugins are available + this.getApiHolder(); + return result; + }, [children]); const loadedConfig = useConfigLoader( this.configLoader, @@ -302,6 +312,7 @@ export class PrivateAppImpl implements BackstageApp { routeParents={routeParents} routeObjects={routeObjects} routeBindings={generateBoundRoutes(this.bindRoutes)} + basePath={getBasePath(loadedConfig.api)} > {children} @@ -314,10 +325,8 @@ export class PrivateAppImpl implements BackstageApp { } getRouter(): ComponentType<{}> { - const { - Router: RouterComponent, - SignInPage: SignInPageComponent, - } = this.components; + const { Router: RouterComponent, SignInPage: SignInPageComponent } = + this.components; // This wraps the sign-in page and waits for sign-in to be completed before rendering the app const SignInPageWrapper = ({ @@ -339,14 +348,7 @@ export class PrivateAppImpl implements BackstageApp { const AppRouter = ({ children }: PropsWithChildren<{}>) => { const configApi = useApi(configApiRef); - - let { pathname } = new URL( - configApi.getOptionalString('app.baseUrl') ?? '/', - 'http://dummy.dev', // baseUrl can be specified as just a path - ); - if (pathname.endsWith('/')) { - pathname = pathname.replace(/\/$/, ''); - } + const mountPath = `${getBasePath(configApi)}/*`; // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { @@ -361,7 +363,7 @@ export class PrivateAppImpl implements BackstageApp { return ( - {children}} /> + {children}} /> ); @@ -371,7 +373,7 @@ export class PrivateAppImpl implements BackstageApp { - {children}} /> + {children}} /> diff --git a/packages/core-app-api/src/app/AppContext.test.tsx b/packages/core-app-api/src/app/AppContext.test.tsx index d87bf7d228..a1131969f2 100644 --- a/packages/core-app-api/src/app/AppContext.test.tsx +++ b/packages/core-app-api/src/app/AppContext.test.tsx @@ -14,20 +14,16 @@ * limitations under the License. */ -import React, { useContext, Context } from 'react'; +import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { VersionedValue } from '../lib/versionedValues'; -import { getGlobalSingleton } from '../lib/globalObject'; +import { useVersionedContext } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; import { AppContextProvider } from './AppContext'; describe('v1 consumer', () => { - const AppContext = getGlobalSingleton< - Context> - >('app-context'); - function useMockAppV1(): AppContextV1 { - const impl = useContext(AppContext)?.atVersion(1); + const impl = + useVersionedContext<{ 1: AppContextV1 }>('app-context')?.atVersion(1); if (!impl) { throw new Error('no impl'); } diff --git a/packages/core-app-api/src/app/AppContext.tsx b/packages/core-app-api/src/app/AppContext.tsx index c583478ec2..f39d9a095e 100644 --- a/packages/core-app-api/src/app/AppContext.tsx +++ b/packages/core-app-api/src/app/AppContext.tsx @@ -14,18 +14,14 @@ * limitations under the License. */ -import React, { createContext, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; import { - VersionedValue, createVersionedValueMap, -} from '../lib/versionedValues'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; + createVersionedContext, +} from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; -type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined; -const AppContext = getOrCreateGlobalSingleton('app-context', () => - createContext(undefined), -); +const AppContext = createVersionedContext<{ 1: AppContextV1 }>('app-context'); type Props = { appContext: AppContextV1; diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index b7c49939e2..1d74f93add 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -60,7 +60,7 @@ export const defaultConfigLoader: AppConfigLoader = async ( if (!Array.isArray(appConfig)) { throw new Error('Static configuration has invalid format'); } - const configs = (appConfig.slice() as unknown) as AppConfig[]; + const configs = appConfig.slice() as unknown as AppConfig[]; // Avoiding this string also being replaced at runtime if ( diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx index bf45155096..a102026b29 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/core-app-api/src/app/icons.tsx @@ -15,17 +15,22 @@ */ import { IconComponent } from '@backstage/core-plugin-api'; -import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; +import MuiApartmentIcon from '@material-ui/icons/Apartment'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiCategoryIcon from '@material-ui/icons/Category'; import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiDocsIcon from '@material-ui/icons/Description'; import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiExtensionIcon from '@material-ui/icons/Extension'; import MuiGitHubIcon from '@material-ui/icons/GitHub'; import MuiHelpIcon from '@material-ui/icons/Help'; +import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; +import MuiMemoryIcon from '@material-ui/icons/Memory'; +import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -import MuiDocsIcon from '@material-ui/icons/Description'; type AppIconsKey = | 'brokenImage' @@ -37,6 +42,13 @@ type AppIconsKey = | 'github' | 'group' | 'help' + | 'kind:api' + | 'kind:component' + | 'kind:domain' + | 'kind:group' + | 'kind:location' + | 'kind:system' + | 'kind:user' | 'user' | 'warning'; @@ -53,6 +65,13 @@ export const defaultAppIcons: AppIcons = { github: MuiGitHubIcon, group: MuiPeopleIcon, help: MuiHelpIcon, + 'kind:api': MuiExtensionIcon, + 'kind:component': MuiMemoryIcon, + 'kind:domain': MuiApartmentIcon, + 'kind:group': MuiPeopleIcon, + 'kind:location': MuiLocationOnIcon, + 'kind:system': MuiCategoryIcon, + 'kind:user': MuiPersonIcon, user: MuiPersonIcon, warning: MuiWarningIcon, }; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 836f1b7fb0..30ce45abe9 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -105,14 +105,14 @@ type KeysWithType = { */ type PartialKeys< Map extends { [name in string]: any }, - Keys extends keyof Map + Keys extends keyof Map, > = Partial> & Required>; /** * Creates a map of target routes with matching parameters based on a map of external routes. */ type TargetRouteMap< - ExternalRoutes extends { [name: string]: ExternalRouteRef } + ExternalRoutes extends { [name: string]: ExternalRouteRef }, > = { [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< infer Params, @@ -123,7 +123,7 @@ type TargetRouteMap< }; export type AppRouteBinder = < - ExternalRoutes extends { [name: string]: ExternalRouteRef } + ExternalRoutes extends { [name: string]: ExternalRouteRef }, >( externalRoutes: ExternalRoutes, targetRoutes: PartialKeys< diff --git a/packages/core-app-api/src/extensions/componentData.test.tsx b/packages/core-app-api/src/extensions/componentData.test.tsx deleted file mode 100644 index 808ab08cf1..0000000000 --- a/packages/core-app-api/src/extensions/componentData.test.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { attachComponentData, getComponentData } from './componentData'; - -describe('elementData', () => { - it('should attach a single piece of data', () => { - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - - it('should attach several distinct pieces of data', () => { - const data1 = { foo: 'bar' }; - const data2 = { test: 'value' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data1); - attachComponentData(Component, 'second', data2); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data1); - expect(getComponentData(element, 'second')).toBe(data2); - }); - - it('returns undefined for missing data', () => { - const data = { foo: 'bar' }; - const Component1 = () => null; - const Component2 = () => null; - attachComponentData(Component2, 'my-data', data); - - const element1 = ; - const element2 = ; - expect(getComponentData(element1, 'missing')).toBeUndefined(); - expect(getComponentData(element2, 'missing')).toBeUndefined(); - }); - - it('should throw when attempting to overwrite data', () => { - const data = { foo: 'bar' }; - const MyComponent = () => null; - attachComponentData(MyComponent, 'my-data', data); - expect(() => attachComponentData(MyComponent, 'my-data', data)).toThrow( - 'Attempted to attach duplicate data "my-data" to component "MyComponent"', - ); - }); - - describe('works across versions', () => { - function getDataSymbol() { - const Component = () => null; - attachComponentData(Component, 'my-data', {}); - const [symbol] = Object.getOwnPropertySymbols(Component); - return symbol; - } - - it('should should be able to get data from older versions', () => { - const symbol = getDataSymbol(); - - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - expect((element as any).type[symbol].map.get('my-data')).toBe(data); - }); - - it('should should be able to attach data for older versions', () => { - const symbol = getDataSymbol(); - - const data = { foo: 'bar' }; - const Component = () => null; - (Component as any)[symbol] = { - map: new Map([['my-data', data]]), - }; - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - - it('should be able to get data from newer versions', () => { - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - const container = (global as any)[ - '__@backstage/component-data-store__' - ].get(element.type); - expect(container.map.get('my-data')).toBe(data); - }); - - it('should should be able to attach data for newer versions', () => { - const data = { foo: 'bar' }; - const Component = () => null; - (global as any)['__@backstage/component-data-store__'].set(Component, { - map: new Map([['my-data', data]]), - }); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - }); -}); diff --git a/packages/core-app-api/src/extensions/componentData.tsx b/packages/core-app-api/src/extensions/componentData.tsx deleted file mode 100644 index d4975d9eef..0000000000 --- a/packages/core-app-api/src/extensions/componentData.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { ComponentType, ReactNode } from 'react'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; - -// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x -const DATA_KEY = Symbol('backstage-component-data'); - -type ComponentWithData

= ComponentType

& { - [DATA_KEY]?: DataContainer; -}; - -type DataContainer = { - map: Map; -}; - -type MaybeComponentNode = ReactNode & { - type?: ComponentType & { [DATA_KEY]?: DataContainer }; -}; - -// The store is bridged across versions using the global object -const store = getOrCreateGlobalSingleton( - 'component-data-store', - () => new WeakMap, DataContainer>(), -); - -export function attachComponentData

( - component: ComponentType

, - type: string, - data: unknown, -) { - const dataComponent = component as ComponentWithData

; - - let container = store.get(component) || dataComponent[DATA_KEY]; - if (!container) { - container = { map: new Map() }; - store.set(component, container); - dataComponent[DATA_KEY] = container; - } - - if (container.map.has(type)) { - const name = component.displayName || component.name; - throw new Error( - `Attempted to attach duplicate data "${type}" to component "${name}"`, - ); - } - - container.map.set(type, data); -} - -export function getComponentData( - node: ReactNode, - type: string, -): T | undefined { - if (!node) { - return undefined; - } - - const component = (node as MaybeComponentNode).type; - if (!component) { - return undefined; - } - - const container = store.get(component) || component[DATA_KEY]; - if (!container) { - return undefined; - } - - return container.map.get(type) as T | undefined; -} diff --git a/packages/core-app-api/src/extensions/extensions.tsx b/packages/core-app-api/src/extensions/extensions.tsx deleted file mode 100644 index 063f34c659..0000000000 --- a/packages/core-app-api/src/extensions/extensions.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { Extension, RouteRef } from '@backstage/core-plugin-api'; - -type ComponentLoader = - | { - lazy: () => Promise; - } - | { - sync: T; - }; - -const ERROR_MESSAGE = 'Import this from @backstage/core-plugin-api'; - -/** @deprecated Import from @backstage/core-plugin-api instead */ -export function createRoutableExtension< - T extends (props: any) => JSX.Element | null ->(_options: { - component: () => Promise; - mountPoint: RouteRef; -}): Extension { - throw new Error(ERROR_MESSAGE); -} - -/** @deprecated Import from @backstage/core-plugin-api instead */ -export function createComponentExtension< - T extends (props: any) => JSX.Element | null ->(_options: { component: ComponentLoader }): Extension { - throw new Error(ERROR_MESSAGE); -} - -/** @deprecated Import from @backstage/core-plugin-api instead */ -export function createReactExtension< - T extends (props: any) => JSX.Element | null ->(_options: { - component: ComponentLoader; - data?: Record; -}): Extension { - throw new Error(ERROR_MESSAGE); -} diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 524a0c5709..28125f6fa8 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -126,8 +126,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: - 'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production', + url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&env=production', }); await expect(sessionPromise).resolves.toEqual({ @@ -175,8 +174,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: - 'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production', + url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&env=production', }); }); }); diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 81da04010c..261a008d4b 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -61,7 +61,8 @@ function defaultJoinScopes(scopes: Set) { * via the OAuthRequestApi. */ export class DefaultAuthConnector - implements AuthConnector { + implements AuthConnector +{ private readonly discoveryApi: DiscoveryApi; private readonly environment: string; private readonly provider: AuthProvider & { id: string }; @@ -151,7 +152,10 @@ export class DefaultAuthConnector private async showPopup(scopes: Set): Promise { const scope = this.joinScopesFunc(scopes); - const popupUrl = await this.buildUrl('/start', { scope }); + const popupUrl = await this.buildUrl('/start', { + scope, + origin: location.origin, + }); const payload = await showLoginPopup({ url: popupUrl, diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts index 4ceadd51ba..ded26c59e5 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts @@ -128,6 +128,19 @@ describe('GheAuth AuthSessionStore', () => { expect(manager.removeSession).toHaveBeenCalled(); }); + it('should set session', async () => { + const manager = new MockManager(); + const store = new AuthSessionStore({ manager, ...defaultOptions }); + + await expect(store.getSession({ optional: true })).resolves.toBe(undefined); + expect(localStorage.getItem('my-key')).toBe(null); + expect(manager.setSession).not.toHaveBeenCalled(); + store.setSession('123'); + expect(manager.setSession).toHaveBeenCalled(); + expect(localStorage.getItem('my-key')).toBe('"123"'); + await expect(store.getSession({ optional: true })).resolves.toBe('123'); + }); + it('should forward sessionState calls', () => { const manager = new MockManager(); const store = new AuthSessionStore({ manager, ...defaultOptions }); diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts index 057a70e58d..fc15da7af8 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -15,7 +15,6 @@ */ import { - SessionManager, MutableSessionManager, SessionScopesFunc, SessionShouldRefreshFunc, @@ -40,7 +39,7 @@ type Options = { * * Session is serialized to JSON with special support for following types: Set. */ -export class AuthSessionStore implements SessionManager { +export class AuthSessionStore implements MutableSessionManager { private readonly manager: MutableSessionManager; private readonly storageKey: string; private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc; @@ -63,6 +62,11 @@ export class AuthSessionStore implements SessionManager { }); } + setSession(session: T | undefined): void { + this.manager.setSession(session); + this.saveSession(session); + } + async getSession(options: GetSessionOptions): Promise { const { scopes } = options; const session = this.loadSession(); diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts new file mode 100644 index 0000000000..919948f531 --- /dev/null +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { Observable, SessionState } from '@backstage/core-plugin-api'; +import { OptionalRefreshSessionManagerMux } from './OptionalRefreshSessionManagerMux'; +import { MutableSessionManager, SessionManager } from './types'; + +class MockManager implements MutableSessionManager { + constructor(public session?: string) {} + + setSession(session: string | undefined): void { + this.session = session; + } + async getSession(): Promise { + return this.session; + } + async removeSession(): Promise { + delete this.session; + } + sessionState$(): Observable { + throw new Error('Method not implemented.'); + } +} + +function trackState(manager: SessionManager) { + const states = new Array(); + manager + .sessionState$() + .subscribe(state => states.push(state === SessionState.SignedIn)); + return states; +} + +describe('OptionalRefreshSessionManagerMux', () => { + it('finds no session', async () => { + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager: new MockManager(), + refreshingSessionManager: new MockManager(), + sessionCanRefresh: () => false, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe(undefined); + expect(states).toEqual([false]); + }); + + it('prioritizes a static session', async () => { + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager: new MockManager('static'), + refreshingSessionManager: new MockManager('refreshing'), + sessionCanRefresh: () => false, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('static'); + expect(states).toEqual([false, true]); + }); + + it('transfers a refreshing session to the static manager', async () => { + const staticSessionManager = new MockManager(); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => false, + }); + + const states = trackState(mux); + expect(staticSessionManager.session).toBeUndefined(); + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBe('refreshing'); + expect(states).toEqual([false, true]); + }); + + it('relies on the refreshing manager if refresh is available', async () => { + const staticSessionManager = new MockManager(); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => true, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBeUndefined(); + expect(states).toEqual([false, true]); + }); + + it('can switch between refreshing and static sessions', async () => { + let canRefresh = true; + const staticSessionManager = new MockManager(); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => canRefresh, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBeUndefined(); + canRefresh = false; + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBe('refreshing'); + + expect(states).toEqual([false, true]); + }); + + it('removes sessions from both managers', async () => { + const staticSessionManager = new MockManager('static'); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => true, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('static'); + await mux.removeSession(); + expect(staticSessionManager.session).toBeUndefined(); + expect(refreshingSessionManager.session).toBeUndefined(); + expect(states).toEqual([false, true, false]); + }); +}); diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts new file mode 100644 index 0000000000..221a7635ea --- /dev/null +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { Observable, SessionState } from '@backstage/core-plugin-api'; +import { + SessionManager, + MutableSessionManager, + GetSessionOptions, +} from './types'; +import { SessionStateTracker } from './SessionStateTracker'; + +type Options = { + /** + * A callback that is called to determine whether a given session supports refresh + */ + sessionCanRefresh: (session: T) => boolean; + + /** + * The session manager that is used if the a session does not support refresh. + */ + staticSessionManager: MutableSessionManager; + + /** + * The session manager that is used if the a session supports refresh. + */ + refreshingSessionManager: SessionManager; +}; + +/** + * OptionalRefreshSessionManagerMux wraps two different session managers, one for + * static session storage and another one that supports refresh. For each session + * that is retrieved is checked for whether it supports refresh. If it does, the + * refreshing session manager is used, otherwise the static session manager is used. + */ +export class OptionalRefreshSessionManagerMux implements SessionManager { + private readonly stateTracker = new SessionStateTracker(); + + private readonly sessionCanRefresh: (session: T) => boolean; + private readonly staticSessionManager: MutableSessionManager; + private readonly refreshingSessionManager: SessionManager; + + constructor(options: Options) { + this.sessionCanRefresh = options.sessionCanRefresh; + this.staticSessionManager = options.staticSessionManager; + this.refreshingSessionManager = options.refreshingSessionManager; + } + + async getSession(options: GetSessionOptions): Promise { + // First we check if there is an existing static session, using an optional request + const staticSession = await this.staticSessionManager.getSession({ + ...options, + optional: true, + }); + if (staticSession) { + this.stateTracker.setIsSignedIn(true); + return staticSession; + } + + // If there is no static session available, we ask the refresh manager to get a session + const session = await this.refreshingSessionManager.getSession(options); + + // Handling the case where the session request is optional + if (!session) { + this.stateTracker.setIsSignedIn(false); + return undefined; + } + + // Next we check if the session we received from the refreshing manager can actually + // be refreshed. If it can, we use this session without storing it in the static manager. + if (this.sessionCanRefresh(session)) { + this.stateTracker.setIsSignedIn(true); + return session; + } + + // If the session can't be refreshed, we store it in the static manager + this.staticSessionManager.setSession(session); + this.stateTracker.setIsSignedIn(true); + return session; + } + + async removeSession(): Promise { + await Promise.all([ + this.refreshingSessionManager.removeSession(), + this.staticSessionManager.removeSession(), + ]); + this.stateTracker.setIsSignedIn(false); + } + + sessionState$(): Observable { + return this.stateTracker.sessionState$(); + } +} diff --git a/packages/core-app-api/src/lib/globalObject.test.ts b/packages/core-app-api/src/lib/globalObject.test.ts deleted file mode 100644 index a658b253a6..0000000000 --- a/packages/core-app-api/src/lib/globalObject.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { - getGlobalSingleton, - getOrCreateGlobalSingleton, - setGlobalSingleton, -} from './globalObject'; - -const anyGlobal = global as any; - -describe('getGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should return an existing value', () => { - const myThing = {}; - const myOtherThing = {}; - - anyGlobal['__@backstage/my-thing__'] = myThing; - expect(getGlobalSingleton('my-thing')).toBe(myThing); - expect(getGlobalSingleton('my-thing')).toBe(myThing); - anyGlobal['__@backstage/my-thing__'] = myOtherThing; - expect(getGlobalSingleton('my-thing')).toBe(myOtherThing); - }); - - it('should throw if the value is not set', () => { - expect(() => getGlobalSingleton('my-thing')).toThrow( - 'Global my-thing is not set', - ); - }); -}); - -describe('getOrCreateGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should return an existing value', () => { - const myThing = {}; - anyGlobal['__@backstage/my-thing__'] = myThing; - - expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myThing); - expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myThing); - }); - - it('should should create a new value', () => { - const myNewThing = {}; - - expect(anyGlobal['__@backstage/my-thing__']).toBe(undefined); - expect(getOrCreateGlobalSingleton('my-thing', () => myNewThing)).toBe( - myNewThing, - ); - expect(anyGlobal['__@backstage/my-thing__']).toBe(myNewThing); - expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myNewThing); - }); -}); - -describe('setGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should set a global value', () => { - setGlobalSingleton('my-thing', 'global value'); - - expect(anyGlobal['__@backstage/my-thing__']).toBe('global value'); - }); - - it('should throw if global value is set', () => { - anyGlobal['__@backstage/my-thing__'] = 'already defined'; - expect(() => setGlobalSingleton('my-thing', () => 'global value')).toThrow( - 'Global my-thing is already se', - ); - }); -}); diff --git a/packages/core-app-api/src/lib/globalObject.ts b/packages/core-app-api/src/lib/globalObject.ts deleted file mode 100644 index ad70a61110..0000000000 --- a/packages/core-app-api/src/lib/globalObject.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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. - */ - -// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 -function getGlobalObject() { - if (typeof window !== 'undefined' && window.Math === Math) { - return window; - } - if (typeof self !== 'undefined' && self.Math === Math) { - return self; - } - // eslint-disable-next-line no-new-func - return Function('return this')(); -} - -const globalObject = getGlobalObject(); - -const makeKey = (id: string) => `__@backstage/${id}__`; - -/** - * Used to provide a global singleton value, failing if it is already set. - */ -export function setGlobalSingleton(id: string, value: unknown): void { - const key = makeKey(id); - if (key in globalObject) { - throw new Error(`Global ${id} is already set`); // TODO some sort of special build err - } - globalObject[key] = value; -} - -/** - * Used to access a global singleton value, failing if it is not already set. - */ -export function getGlobalSingleton(id: string): T { - const key = makeKey(id); - if (!(key in globalObject)) { - throw new Error(`Global ${id} is not set`); // TODO some sort of special build err - } - - return globalObject[key]; -} - -/** - * Serializes access to a global singleton value, with the first caller creating the value. - */ -export function getOrCreateGlobalSingleton( - id: string, - supplier: () => T, -): T { - const key = makeKey(id); - - let value = globalObject[key]; - if (value) { - return value; - } - - value = supplier(); - globalObject[key] = value; - return value; -} diff --git a/packages/core-app-api/src/lib/loginPopup.test.ts b/packages/core-app-api/src/lib/loginPopup.test.ts index 1eb7c3f8b9..db4d9eb8eb 100644 --- a/packages/core-app-api/src/lib/loginPopup.test.ts +++ b/packages/core-app-api/src/lib/loginPopup.test.ts @@ -30,8 +30,7 @@ describe('showLoginPopup', () => { const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); const payloadPromise = showLoginPopup({ - url: - 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', + url: 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', name: 'test-popup', origin: 'my-origin', }); diff --git a/packages/core-app-api/src/lib/subjects.ts b/packages/core-app-api/src/lib/subjects.ts index 0391050bda..e122da819b 100644 --- a/packages/core-app-api/src/lib/subjects.ts +++ b/packages/core-app-api/src/lib/subjects.ts @@ -29,7 +29,8 @@ import ObservableImpl from 'zen-observable'; * See http://reactivex.io/documentation/subject.html */ export class PublishSubject - implements Observable, ZenObservable.SubscriptionObserver { + implements Observable, ZenObservable.SubscriptionObserver +{ private isClosed = false; private terminatingError?: Error; @@ -121,7 +122,8 @@ export class PublishSubject * See http://reactivex.io/documentation/subject.html */ export class BehaviorSubject - implements Observable, ZenObservable.SubscriptionObserver { + implements Observable, ZenObservable.SubscriptionObserver +{ private isClosed = false; private currentValue: T; private terminatingError?: Error; diff --git a/packages/core-app-api/src/plugins/collectors.ts b/packages/core-app-api/src/plugins/collectors.ts index 11e5cd51b3..0f8b65ad62 100644 --- a/packages/core-app-api/src/plugins/collectors.ts +++ b/packages/core-app-api/src/plugins/collectors.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { getComponentData } from '../extensions'; +import { BackstagePlugin, getComponentData } from '@backstage/core-plugin-api'; import { createCollector } from '../extensions/traversal'; export const pluginCollector = createCollector( diff --git a/packages/core-app-api/src/routing/FlatRoutes.test.tsx b/packages/core-app-api/src/routing/FlatRoutes.test.tsx index a73534a272..0df296cc59 100644 --- a/packages/core-app-api/src/routing/FlatRoutes.test.tsx +++ b/packages/core-app-api/src/routing/FlatRoutes.test.tsx @@ -37,11 +37,11 @@ function makeRouteRenderer(node: ReactNode) { ({ NotFoundErrorPage: () => <>Not Found, }), - } as unknown) as AppContext + } as unknown as AppContext } > @@ -100,7 +100,6 @@ describe('FlatRoutes', () => { return <>Outlet: {useOutlet()}; }; - // The '/*' suffixes here are intentional and will be ignored by FlatRoutes const routes = ( <> }> @@ -112,11 +111,15 @@ describe('FlatRoutes', () => { }> b + }> + c + ); const renderRoute = makeRouteRenderer({routes}); expect(renderRoute('/a').getByText('Outlet: a')).toBeInTheDocument(); expect(renderRoute('/a/b').getByText('Outlet: a-b')).toBeInTheDocument(); expect(renderRoute('/b').getByText('Outlet: b')).toBeInTheDocument(); + expect(renderRoute('/').getByText('Outlet: c')).toBeInTheDocument(); }); }); diff --git a/packages/core-app-api/src/routing/FlatRoutes.tsx b/packages/core-app-api/src/routing/FlatRoutes.tsx index 6ba82203fd..315ec91cdb 100644 --- a/packages/core-app-api/src/routing/FlatRoutes.tsx +++ b/packages/core-app-api/src/routing/FlatRoutes.tsx @@ -49,8 +49,10 @@ export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { element: child, children: child.props.children ? [ + // These are the children of each route, which we all add in under a catch-all + // subroute in order to make them available to `useOutlet` { - path: '/*', + path: path === '/' ? '/' : '/*', // The root path must require an exact match element: child.props.children, }, ] diff --git a/packages/core-app-api/src/routing/RouteResolver.test.ts b/packages/core-app-api/src/routing/RouteResolver.test.ts index b47a18ecce..1b7eb10003 100644 --- a/packages/core-app-api/src/routing/RouteResolver.test.ts +++ b/packages/core-app-api/src/routing/RouteResolver.test.ts @@ -65,7 +65,7 @@ const externalRef4 = createExternalRouteRef({ describe('RouteResolver', () => { it('should not resolve anything with an empty resolver', () => { - const r = new RouteResolver(new Map(), new Map(), [], new Map()); + const r = new RouteResolver(new Map(), new Map(), [], new Map(), ''); expect(r.resolve(ref1, '/')?.()).toBe(undefined); expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined); @@ -85,6 +85,7 @@ describe('RouteResolver', () => { new Map(), [{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }], new Map(), + '', ); expect(r.resolve(ref1, '/')?.()).toBe('/my-route'); @@ -99,6 +100,29 @@ describe('RouteResolver', () => { expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined); }); + it('should resolve an absolute route and an app base path', () => { + const r = new RouteResolver( + new Map([[ref1, '/my-route']]), + new Map(), + [{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }], + new Map(), + '/base', + ); + + expect(r.resolve(ref1, '/')?.()).toBe('/base/my-route'); + expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined); + expect(r.resolve(subRef1, '/')?.()).toBe('/base/my-route/foo'); + expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe( + '/base/my-route/foo/2a', + ); + expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined); + expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined); + expect(r.resolve(externalRef1, '/')?.()).toBe(undefined); + expect(r.resolve(externalRef2, '/')?.()).toBe(undefined); + expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined); + expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined); + }); + it('should resolve an absolute route with a param and with a parent', () => { const r = new RouteResolver( new Map([ @@ -122,6 +146,7 @@ describe('RouteResolver', () => { [externalRef3, ref2], [externalRef4, subRef3], ]), + '', ); expect(r.resolve(ref1, '/')?.()).toBe('/my-route'); @@ -179,6 +204,7 @@ describe('RouteResolver', () => { }, ], new Map(), + '', ); expect(r.resolve(ref2, '/')?.({ x: 'x' })).toBe('/root/x'); @@ -232,6 +258,7 @@ describe('RouteResolver', () => { [externalRef3, ref2], [externalRef4, subRef3], ]), + '', ); const l = '/my-grandparent/my-y/my-parent/my-x'; diff --git a/packages/core-app-api/src/routing/RouteResolver.ts b/packages/core-app-api/src/routing/RouteResolver.ts index e09da28df8..b5be5f893e 100644 --- a/packages/core-app-api/src/routing/RouteResolver.ts +++ b/packages/core-app-api/src/routing/RouteResolver.ts @@ -187,6 +187,7 @@ export class RouteResolver { ExternalRouteRef, RouteRef | SubRouteRef >, + private readonly appBasePath: string, // base path without a trailing slash ) {} resolve( @@ -209,13 +210,15 @@ export class RouteResolver { // Next we figure out the base path, which is the combination of the common parent path // between our current location and our target location, as well as the additional path // that is the difference between the parent path and the base of our target location. - const basePath = resolveBasePath( - targetRef, - sourceLocation, - this.routePaths, - this.routeParents, - this.routeObjects, - ); + const basePath = + this.appBasePath + + resolveBasePath( + targetRef, + sourceLocation, + this.routePaths, + this.routeParents, + this.routeObjects, + ); const routeFunc: RouteFunc = (...[params]) => { return basePath + generatePath(targetPath, params); diff --git a/packages/core-app-api/src/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx index 499b81a743..397217e34a 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx @@ -14,17 +14,11 @@ * limitations under the License. */ -import React, { - PropsWithChildren, - ReactElement, - useContext, - Context, -} from 'react'; +import React, { PropsWithChildren, ReactElement } from 'react'; import { MemoryRouter, Routes } from 'react-router-dom'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; -import { VersionedValue } from '../lib/versionedValues'; -import { getGlobalSingleton } from '../lib/globalObject'; +import { useVersionedContext } from '@backstage/version-bridge'; import { childDiscoverer, routeElementDiscoverer, @@ -152,6 +146,7 @@ function withRoutingProvider( routeParents={routeParents} routeObjects={routeObjects} routeBindings={new Map(routeBindings)} + basePath="" > {root} @@ -330,15 +325,13 @@ describe('discovery', () => { }); describe('v1 consumer', () => { - const RoutingContext = getGlobalSingleton< - Context> - >('routing-context'); - function useMockRouteRefV1( routeRef: AnyRouteRef, location: string, ): RouteFunc | undefined { - const resolver = useContext(RoutingContext)?.atVersion(1); + const resolver = useVersionedContext<{ + 1: RouteResolver; + }>('routing-context')?.atVersion(1); if (!resolver) { throw new Error('no impl'); } @@ -367,6 +360,7 @@ describe('v1 consumer', () => { routeParents={new Map()} routeObjects={[]} routeBindings={new Map()} + basePath="/base" children={children} /> ), @@ -375,8 +369,8 @@ describe('v1 consumer', () => { expect(renderedHook.result.current).toBe(undefined); renderedHook.rerender({ routeRef: routeRef2 }); - expect(renderedHook.result.current?.()).toBe('/foo'); + expect(renderedHook.result.current?.()).toBe('/base/foo'); renderedHook.rerender({ routeRef: routeRef3 }); - expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/bar/my-x'); + expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/base/bar/my-x'); }); }); diff --git a/packages/core-app-api/src/routing/RoutingProvider.tsx b/packages/core-app-api/src/routing/RoutingProvider.tsx index ea8e8f76f8..2ab1686368 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.tsx @@ -14,30 +14,28 @@ * limitations under the License. */ -import React, { createContext, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { ExternalRouteRef, RouteRef, SubRouteRef, } from '@backstage/core-plugin-api'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; import { createVersionedValueMap, - VersionedValue, -} from '../lib/versionedValues'; + createVersionedContext, +} from '@backstage/version-bridge'; import { RouteResolver } from './RouteResolver'; import { BackstageRouteObject } from './types'; -type RoutingContextType = VersionedValue<{ 1: RouteResolver }> | undefined; -const RoutingContext = getOrCreateGlobalSingleton('routing-context', () => - createContext(undefined), -); +const RoutingContext = + createVersionedContext<{ 1: RouteResolver }>('routing-context'); type ProviderProps = { routePaths: Map; routeParents: Map; routeObjects: BackstageRouteObject[]; routeBindings: Map; + basePath?: string; children: ReactNode; }; @@ -46,6 +44,7 @@ export const RoutingProvider = ({ routeParents, routeObjects, routeBindings, + basePath = '', children, }: ProviderProps) => { const resolver = new RouteResolver( @@ -53,6 +52,7 @@ export const RoutingProvider = ({ routeParents, routeObjects, routeBindings, + basePath, ); const versionedValue = createVersionedValueMap({ 1: resolver }); diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.test.tsx index d37b5e3016..36d99d2938 100644 --- a/packages/core-app-api/src/routing/collectors.test.tsx +++ b/packages/core-app-api/src/routing/collectors.test.tsx @@ -31,8 +31,8 @@ import { createRouteRef, createPlugin, RouteRef, + attachComponentData, } from '@backstage/core-plugin-api'; -import { attachComponentData } from '../extensions'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx index fcbe3ca923..0ba83e90f8 100644 --- a/packages/core-app-api/src/routing/collectors.tsx +++ b/packages/core-app-api/src/routing/collectors.tsx @@ -15,9 +15,8 @@ */ import { isValidElement, ReactElement, ReactNode } from 'react'; -import { RouteRef } from '@backstage/core-plugin-api'; +import { RouteRef, getComponentData } from '@backstage/core-plugin-api'; import { BackstageRouteObject } from './types'; -import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged'; diff --git a/packages/core-app-api/src/routing/types.ts b/packages/core-app-api/src/routing/types.ts index 6561a0da70..12bf0d1a0f 100644 --- a/packages/core-app-api/src/routing/types.ts +++ b/packages/core-app-api/src/routing/types.ts @@ -19,7 +19,7 @@ import { SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; type RouteRefType = Exclude< keyof RouteRef, @@ -75,7 +75,7 @@ export function isSubRouteRef( export function isExternalRouteRef< Params extends AnyParams, - Optional extends boolean + Optional extends boolean, >( routeRef: | RouteRef diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 1536cf3fb2..1c195cdd2c 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,141 @@ # @backstage/core-components +## 0.4.1 + +### Patch Changes + +- 06e275705: Fix warning produced by BottomLink component + + During development, we noticed warnings such as: + + ``` + react_devtools_backend.js:2842 Warning: validateDOMNesting(...):

cannot appear as a descendant of

. + ``` + + The BottomLink component renders a Box component within a Typography component which leads to a div tag within a p tag. + This change inverts that ordering without changing the visual appearance. + +- Updated dependencies + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + - @backstage/core-plugin-api@0.1.7 + +## 0.4.0 + +### Minor Changes + +- 3ed78fca3: Changed the `titleComponent` prop on `ContentHeader` to accept `ReactNode` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: + + ```diff + - + +}> + ``` + +### Patch Changes + +- e0a6aea82: Bumped `react-hook-form` to `^7.12.2` + +## 0.3.3 + +### Patch Changes + +- d041655a7: Fix accessibility issue in ``. The component doesn't render anymore an hidden `textarea` containing the text to be copied. +- 6d76bca85: Handle changes to nodes passed into `` correctly. +- Updated dependencies + - @backstage/config@0.1.8 + +## 0.3.2 + +### Patch Changes + +- a3f3cff3b: Change the default hover experience for the sidebar to be not jumpy & add visual separation between sidebar & Entity Page tabs for dark mode. +- 6b1afe8c0: Add a configurable `palette.bursts.gradient` property to the Backstage theme, to support customizing the gradients in the `ItemCard` header. +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/theme@0.2.10 + +## 0.3.1 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- 55a5dbd54: Fix for `SidebarItem` matching the active route too broadly. +- Updated dependencies + - @backstage/core-plugin-api@0.1.6 + +## 0.3.0 + +### Minor Changes + +- 7bf006210: Remove unused props from InfoCard prop type + +### Patch Changes + +- c4d8ff963: Switched frontend identity code to use `token` instead of the deprecated `idToken` field +- 7b8aa8d0d: Move the `CreateComponentButton` from the catalog plugin to the `core-components` & rename it to `CreateButton` to be reused inside the api-docs plugin & scaffolder plugin, but also future plugins. Additionally, improve responsiveness of `CreateButton` & `SupportButton` by shrinking them to `IconButtons` on smaller screens. +- 260c053b9: Fix All Material UI Warnings +- Updated dependencies + - @backstage/config@0.1.6 + - @backstage/core-plugin-api@0.1.5 + +## 0.2.0 + +### Minor Changes + +- 9d40fcb1e: - Bumping `material-ui/core` version to at least `4.12.2` as they made some breaking changes in later versions which broke `Pagination` of the `Table`. + - Switching out `material-table` to `@material-table/core` for support for the later versions of `material-ui/core` + - This causes a minor API change to `@backstage/core-components` as the interface for `Table` re-exports the `prop` from the underlying `Table` components. + - `onChangeRowsPerPage` has been renamed to `onRowsPerPageChange` + - `onChangePage` has been renamed to `onPageChange` + - Migration guide is here: https://material-table-core.com/docs/breaking-changes + +### Patch Changes + +- 19d9995b6: Improve accessibility of core & catalog components by adjusting them with non-breaking changes. +- 224e54484: Added an `EntityProcessingErrorsPanel` component to show any errors that occurred when refreshing an entity from its source location. + + If upgrading, this should be added to your `EntityPage` in your Backstage application: + + ```diff + // packages/app/src/components/catalog/EntityPage.tsx + + const overviewContent = ( + ... + + + + + + + + + + + + + + + + + + + + ``` + + Additionally, `WarningPanel` now changes color based on the provided severity. + +- Updated dependencies + - @backstage/core-plugin-api@0.1.4 + - @backstage/theme@0.2.9 + +## 0.1.6 + +### Patch Changes + +- 9a751bb28: Increase the vertical padding of the sidebar search input field to match the height of the parent anchor tag. This prevents users from accidentally navigating to the search page when they actually wanted to use the search input directly. +- 45b5fc3a8: Updated the layout of catalog and API index pages to handle smaller screen sizes. This adds responsive wrappers to the entity tables, and switches filters to a drawer when width-constrained. If you have created a custom catalog or API index page, you will need to update the page structure to match the updated [catalog customization](https://backstage.io/docs/features/software-catalog/catalog-customization) documentation. +- 03bf17e9b: Improve the responsiveness of the EntityPage UI. With this the Header component should scale with the screen size & wrapping should not cause overflowing/blocking of links. Additionally enforce the Pages using the Grid Layout to use it across all screen sizes & to wrap as intended. + + To benefit from the improved responsive layout, the `EntityPage` in existing Backstage applications should be updated to set the `xs` column size on each grid item in the page, as this does not default. For example: + + ```diff + - + + + ``` + ## 0.1.5 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 3d8e60d4fb..f3652d98cf 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -9,13 +9,10 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { Breadcrumbs as Breadcrumbs_2 } from '@material-ui/core'; import { ButtonProps } from '@material-ui/core'; -import { ButtonTypeMap } from '@material-ui/core'; import { CardHeaderProps } from '@material-ui/core'; -import { Column } from 'material-table'; -import { CommonProps } from '@material-ui/core/OverridableComponent'; +import { Column } from '@material-table/core'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; -import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; @@ -26,8 +23,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core'; import { LinkProps as LinkProps_2 } from '@material-ui/core'; import { LinkProps as LinkProps_3 } from 'react-router-dom'; -import { MaterialTableProps } from 'material-table'; +import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; +import { Palette } from '@material-ui/core/styles/createPalette'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; @@ -47,9 +45,13 @@ import { Theme } from '@material-ui/core'; import { TooltipProps } from '@material-ui/core'; import { WithStyles } from '@material-ui/core'; +// Warning: (ae-missing-release-tag) "AlertDisplay" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const AlertDisplay: () => JSX.Element | null; +export function AlertDisplay(_props: {}): JSX.Element | null; +// Warning: (ae-missing-release-tag) "Alignment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) enum Alignment { // (undocumented) @@ -62,381 +64,97 @@ enum Alignment { UP_RIGHT = 'UR', } +// Warning: (ae-forgotten-export) The symbol "AvatarProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Avatar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Avatar: ({ - displayName, - picture, - customStyles, -}: AvatarProps) => JSX.Element; +export function Avatar(props: AvatarProps): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Breadcrumbs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function Breadcrumbs(props: Props_24): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "IconComponentProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "BrokenImageIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function BrokenImageIcon(props: IconComponentProps): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ButtonType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function Button(props: Props): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "CardTabProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "CardTab" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function CardTab(props: PropsWithChildren): JSX.Element; + +// Warning: (ae-missing-release-tag) "CatalogIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function CatalogIcon(props: IconComponentProps): JSX.Element; + +// Warning: (ae-missing-release-tag) "ChatIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function ChatIcon(props: IconComponentProps): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "CodeSnippet" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const CodeSnippet: (props: Props_2) => JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Content" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function Content(props: PropsWithChildren): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ContentHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function ContentHeader( + props: PropsWithChildren, +): JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "CopyTextButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "CopyTextButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function CopyTextButton(props: Props_3): JSX.Element; // @public (undocumented) -export const Breadcrumbs: ({ children, ...props }: Props_25) => JSX.Element; +export namespace CopyTextButton { + var // (undocumented) + propTypes: { + text: PropTypes.Validator; + tooltipDelay: PropTypes.Requireable; + tooltipText: PropTypes.Requireable; + }; +} +// Warning: (ae-forgotten-export) The symbol "CreateButtonProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "CreateButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const BrokenImageIcon: IconComponent; - -// @public -export const Button: React_2.ForwardRefExoticComponent< - Pick< - Props, - | 'replace' - | 'media' - | 'hidden' - | 'dir' - | 'form' - | 'slot' - | 'title' - | 'disabled' - | 'color' - | 'size' - | 'underline' - | 'display' - | 'translate' - | 'prefix' - | 'children' - | 'key' - | 'value' - | 'id' - | 'name' - | 'action' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'accessKey' - | 'contentEditable' - | 'contextMenu' - | 'draggable' - | 'lang' - | 'placeholder' - | 'spellCheck' - | 'tabIndex' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'inputMode' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | 'component' - | 'variant' - | 'download' - | 'href' - | 'hrefLang' - | 'ping' - | 'rel' - | 'target' - | 'type' - | 'referrerPolicy' - | 'disableElevation' - | 'fullWidth' - | 'startIcon' - | 'endIcon' - | 'noWrap' - | 'gutterBottom' - | 'paragraph' - | 'autoFocus' - | 'formAction' - | 'formEncType' - | 'formMethod' - | 'formNoValidate' - | 'formTarget' - | 'disableFocusRipple' - | 'buttonRef' - | 'centerRipple' - | 'disableRipple' - | 'disableTouchRipple' - | 'focusRipple' - | 'focusVisibleClassName' - | 'onFocusVisible' - | 'TouchRippleProps' - | 'align' - | 'variantMapping' - | 'to' - | 'state' - | 'TypographyClasses' - | keyof CommonProps> - > & - React_2.RefAttributes ->; +export function CreateButton(props: CreateButtonProps): JSX.Element | null; +// Warning: (ae-missing-release-tag) "DashboardIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const CardTab: ({ - children, - ...props -}: PropsWithChildren) => JSX.Element; - -// @public (undocumented) -export const CatalogIcon: IconComponent; - -// @public (undocumented) -export const ChatIcon: IconComponent; - -// @public (undocumented) -export const CodeSnippet: ({ - text, - language, - showLineNumbers, - showCopyCodeButton, - highlightedNumbers, - customStyle, -}: Props_2) => JSX.Element; - -// @public (undocumented) -export const Content: ({ - className, - stretch, - noPadding, - children, - ...props -}: PropsWithChildren) => JSX.Element; - -// @public (undocumented) -export const ContentHeader: ({ - description, - title, - titleComponent: TitleComponent, - children, - textAlign, -}: PropsWithChildren) => JSX.Element; - -// @public (undocumented) -export const CopyTextButton: { - (props: Props_3): JSX.Element; - propTypes: { - text: PropTypes.Validator; - tooltipDelay: PropTypes.Requireable; - tooltipText: PropTypes.Requireable; - }; -}; - -// @public (undocumented) -export const DashboardIcon: IconComponent; +export function DashboardIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "CustomType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DependencyEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type DependencyEdge = T & { from: string; @@ -444,28 +162,11 @@ type DependencyEdge = T & { label?: string; }; +// Warning: (ae-forgotten-export) The symbol "DependencyGraphProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DependencyGraph" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export function DependencyGraph({ - edges, - nodes, - renderNode, - direction, - align, - nodeMargin, - edgeMargin, - rankMargin, - paddingX, - paddingY, - acyclicer, - ranker, - labelPosition, - labelOffset, - edgeRanks, - edgeWeight, - renderLabel, - defs, - ...svgProps -}: DependencyGraphProps): JSX.Element; +export function DependencyGraph(props: DependencyGraphProps): JSX.Element; declare namespace DependencyGraphTypes { export { @@ -486,11 +187,15 @@ declare namespace DependencyGraphTypes { } export { DependencyGraphTypes }; +// Warning: (ae-missing-release-tag) "DependencyNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type DependencyNode = T & { id: string; }; +// Warning: (ae-missing-release-tag) "Direction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) enum Direction { // (undocumented) @@ -503,17 +208,19 @@ enum Direction { TOP_BOTTOM = 'TB', } +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DismissableBanner" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const DismissableBanner: ({ - variant, - message, - id, - fixed, -}: Props_4) => JSX.Element; +export const DismissableBanner: (props: Props_4) => JSX.Element; +// Warning: (ae-missing-release-tag) "DocsIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const DocsIcon: IconComponent; +export function DocsIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-missing-release-tag) "EdgeProperties" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type EdgeProperties = { label?: string; @@ -526,41 +233,46 @@ type EdgeProperties = { [customKey: string]: any; }; +// Warning: (ae-missing-release-tag) "EmailIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const EmailIcon: IconComponent; +export function EmailIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "EmptyState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const EmptyState: ({ - title, - description, - missing, - action, -}: Props_5) => JSX.Element; +export function EmptyState(props: Props_5): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ErrorBoundary" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const ErrorBoundary: ComponentClass; +// Warning: (ae-missing-release-tag) "ErrorBoundaryProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type ErrorBoundaryProps = { slackChannel?: string | SlackChannel; onError?: (error: Error, errorInfo: string) => null; }; +// Warning: (ae-forgotten-export) The symbol "IErrorPageProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ErrorPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const ErrorPage: ({ - status, - statusMessage, - additionalInfo, -}: IErrorPageProps) => JSX.Element; +export function ErrorPage(props: IErrorPageProps): JSX.Element; +// Warning: (ae-missing-release-tag) "ErrorPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const ErrorPanel: ({ - title, - error, - defaultExpanded, - children, -}: PropsWithChildren) => JSX.Element; +export function ErrorPanel( + props: PropsWithChildren, +): JSX.Element; +// Warning: (ae-missing-release-tag) "ErrorPanelProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type ErrorPanelProps = { error: Error; @@ -568,74 +280,92 @@ export type ErrorPanelProps = { title?: string; }; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "FeatureCalloutCircular" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const FeatureCalloutCircular: ({ - featureId, - title, - description, - children, -}: PropsWithChildren) => JSX.Element; +export function FeatureCalloutCircular( + props: PropsWithChildren, +): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Gauge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Gauge: (props: Props_14) => JSX.Element; +export function Gauge(props: Props_14): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "GaugeCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const GaugeCard: (props: Props_13) => JSX.Element; +export function GaugeCard(props: Props_13): JSX.Element; +// Warning: (ae-missing-release-tag) "GitHubIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const GitHubIcon: IconComponent; +export function GitHubIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-missing-release-tag) "GraphEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type GraphEdge = DependencyEdge & dagre_2.GraphEdge & EdgeProperties; +// Warning: (ae-missing-release-tag) "GraphNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type GraphNode = dagre_2.Node>; +// Warning: (ae-missing-release-tag) "GroupIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const GroupIcon: IconComponent; +export function GroupIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Header" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Header: ({ - children, - pageTitleOverride, - style, - subtitle, - title, - tooltip, - type, - typeLink, -}: PropsWithChildren) => JSX.Element; +export function Header(props: PropsWithChildren): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "HeaderIconLinkRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const HeaderIconLinkRow: ({ links }: Props_8) => JSX.Element; +export function HeaderIconLinkRow(props: Props_8): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "HeaderLabelProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "HeaderLabel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const HeaderLabel: ({ - label, - value, - url, -}: HeaderLabelProps) => JSX.Element; +export function HeaderLabel(props: HeaderLabelProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "HeaderTabsProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "HeaderTabs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const HeaderTabs: ({ - tabs, - onChange, - selectedIndex, -}: HeaderTabsProps) => JSX.Element; +export function HeaderTabs(props: HeaderTabsProps): JSX.Element; +// Warning: (ae-missing-release-tag) "HelpIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const HelpIcon: IconComponent; +export function HelpIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-missing-release-tag) "HomepageTimer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const HomepageTimer: () => JSX.Element | null; +export function HomepageTimer(_props: {}): JSX.Element | null; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "HorizontalScrollGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const HorizontalScrollGrid: ( +export function HorizontalScrollGrid( props: PropsWithChildren, -) => JSX.Element; +): JSX.Element; +// Warning: (ae-missing-release-tag) "IconLinkVerticalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type IconLinkVerticalProps = { color?: 'primary' | 'secondary'; @@ -647,57 +377,64 @@ export type IconLinkVerticalProps = { title?: string; }; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "InfoCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const InfoCard: ({ - title, - subheader, - divider, - deepLink, - slackChannel, - errorBoundaryProps, - variant, - children, - headerStyle, - headerProps, - action, - actionsClassName, - actions, - cardClassName, - actionsTopRight, - className, - noPadding, - titleTypographyProps, -}: Props_20) => JSX.Element; +export function InfoCard(props: Props_19): JSX.Element; +// Warning: (ae-missing-release-tag) "InfoCardVariants" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; +// Warning: (ae-forgotten-export) The symbol "IntroCardProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IntroCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const IntroCard: (props: IntroCardProps) => JSX.Element; +export function IntroCard(props: IntroCardProps): JSX.Element; +// Warning: (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name +// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// Warning: (ae-forgotten-export) The symbol "ItemCardProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ItemCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public @deprecated -export const ItemCard: ({ - description, - tags, - title, - type, - subtitle, - label, - onClick, - href, -}: ItemCardProps) => JSX.Element; +export function ItemCard(props: ItemCardProps): JSX.Element; +// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-html-tag-missing-string) The HTML element has an invalid attribute: Expecting an HTML string starting with a single-quote or double-quote character +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (ae-missing-release-tag) "ItemCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const ItemCardGrid: (props: ItemCardGridProps) => JSX.Element; +export function ItemCardGrid(props: ItemCardGridProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "styles" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ItemCardGridProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type ItemCardGridProps = Partial> & { children?: React_2.ReactNode; }; +// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-html-tag-missing-string) The HTML element has an invalid attribute: Expecting an HTML string starting with a single-quote or double-quote character +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (ae-missing-release-tag) "ItemCardHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const ItemCardHeader: (props: ItemCardHeaderProps) => JSX.Element; +export function ItemCardHeader(props: ItemCardHeaderProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "styles" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ItemCardHeaderProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type ItemCardHeaderProps = Partial> & { title?: React_2.ReactNode; @@ -705,6 +442,8 @@ export type ItemCardHeaderProps = Partial> & { children?: React_2.ReactNode; }; +// Warning: (ae-missing-release-tag) "LabelPosition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) enum LabelPosition { // (undocumented) @@ -715,330 +454,77 @@ enum LabelPosition { RIGHT = 'r', } +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Lifecycle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Lifecycle: (props: Props_10) => JSX.Element; +export function Lifecycle(props: Props_10): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "LinearGauge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const LinearGauge: ({ value }: Props_15) => JSX.Element | null; +export function LinearGauge(props: Props_15): JSX.Element | null; -// @public -export const Link: React_2.ForwardRefExoticComponent< - Pick< - LinkProps, - | 'replace' - | 'media' - | 'hidden' - | 'dir' - | 'slot' - | 'style' - | 'title' - | 'color' - | 'underline' - | 'display' - | 'translate' - | 'prefix' - | 'children' - | 'key' - | 'id' - | 'classes' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'accessKey' - | 'className' - | 'contentEditable' - | 'contextMenu' - | 'draggable' - | 'lang' - | 'placeholder' - | 'spellCheck' - | 'tabIndex' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'inputMode' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | 'component' - | 'variant' - | 'innerRef' - | 'download' - | 'href' - | 'hrefLang' - | 'ping' - | 'rel' - | 'target' - | 'type' - | 'referrerPolicy' - | 'noWrap' - | 'gutterBottom' - | 'paragraph' - | 'align' - | 'variantMapping' - | 'to' - | 'state' - | 'TypographyClasses' - > & - React_2.RefAttributes ->; +// Warning: (ae-missing-release-tag) "LinkType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function Link(props: LinkProps): JSX.Element; +// Warning: (ae-missing-release-tag) "LinkProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type LinkProps = LinkProps_2 & LinkProps_3 & { component?: ElementType; }; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "MarkdownContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const MarkdownContent: ({ content, dialect }: Props_11) => JSX.Element; +export function MarkdownContent(props: Props_11): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "MissingAnnotationEmptyState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const MissingAnnotationEmptyState: ({ - annotation, -}: Props_6) => JSX.Element; +export function MissingAnnotationEmptyState(props: Props_6): JSX.Element; +// Warning: (ae-missing-release-tag) "OAuthRequestDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const OAuthRequestDialog: () => JSX.Element; +export function OAuthRequestDialog(_props: {}): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "OverflowTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const OverflowTooltip: (props: Props_12) => JSX.Element; +export function OverflowTooltip(props: Props_12): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Page" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Page: ({ - themeId, - children, -}: PropsWithChildren) => JSX.Element; +export function Page(props: PropsWithChildren): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "PageWithHeaderProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "PageWithHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Progress: ( +export function PageWithHeader( + props: PropsWithChildren, +): JSX.Element; + +// Warning: (ae-missing-release-tag) "Progress" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function Progress( props: PropsWithChildren, -) => JSX.Element; +): JSX.Element; +// Warning: (ae-missing-release-tag) "Ranker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) enum Ranker { // (undocumented) @@ -1049,54 +535,60 @@ enum Ranker { TIGHT_TREE = 'tight-tree', } +// Warning: (ae-missing-release-tag) "RenderLabelFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type RenderLabelFunction = (props: RenderLabelProps) => React.ReactNode; +// Warning: (ae-missing-release-tag) "RenderLabelProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type RenderLabelProps = { edge: DependencyEdge; }; +// Warning: (ae-missing-release-tag) "RenderNodeFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type RenderNodeFunction = (props: RenderNodeProps) => React.ReactNode; +// Warning: (ae-missing-release-tag) "RenderNodeProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) type RenderNodeProps = { node: DependencyNode; }; +// Warning: (ae-missing-release-tag) "ResponseErrorPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const ResponseErrorPanel: ({ - title, - error, - defaultExpanded, -}: ErrorPanelProps) => JSX.Element; +export function ResponseErrorPanel(props: ErrorPanelProps): JSX.Element; +// Warning: (ae-missing-release-tag) "RoutedTabs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const RoutedTabs: ({ routes }: { routes: SubRoute_2[] }) => JSX.Element; +export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SelectComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Select: ({ - multiple, - items, - label, - placeholder, - selected, - onChange, - triggerReset, -}: SelectProps) => JSX.Element; +export function Select(props: SelectProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Sidebar: ({ - openDelayMs, - closeDelayMs, - children, -}: PropsWithChildren) => JSX.Element; +export function Sidebar(props: PropsWithChildren): JSX.Element; +// Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SIDEBAR_INTRO_LOCAL_STORAGE = '@backstage/core/sidebar-intro-dismissed'; +// Warning: (ae-missing-release-tag) "sidebarConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const sidebarConfig: { drawerWidthClosed: number; @@ -1113,14 +605,20 @@ export const sidebarConfig: { userBadgeDiameter: number; }; +// Warning: (ae-missing-release-tag) "SidebarContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarContext: Context; +// Warning: (ae-missing-release-tag) "SidebarContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SidebarContextType = { isOpen: boolean; }; +// Warning: (ae-missing-release-tag) "SidebarDivider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarDivider: React_2.ComponentType< Pick< @@ -1388,26 +886,39 @@ export const SidebarDivider: React_2.ComponentType< } >; +// Warning: (ae-missing-release-tag) "SidebarIntro" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SidebarIntro: () => JSX.Element | null; +export function SidebarIntro(_props: {}): JSX.Element | null; +// Warning: (ae-forgotten-export) The symbol "SidebarItemProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SidebarItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarItem: React_2.ForwardRefExoticComponent< SidebarItemProps & React_2.RefAttributes >; +// Warning: (ae-missing-release-tag) "SidebarPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SidebarPage: (props: PropsWithChildren<{}>) => JSX.Element; +export function SidebarPage(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-missing-release-tag) "SidebarPinStateContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarPinStateContext: React_2.Context; +// Warning: (ae-missing-release-tag) "SidebarPinStateContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SidebarPinStateContextType = { isPinned: boolean; toggleSidebarPinState: () => any; }; +// Warning: (ae-missing-release-tag) "SidebarScrollWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarScrollWrapper: React_2.ComponentType< Pick< @@ -1675,11 +1186,14 @@ export const SidebarScrollWrapper: React_2.ComponentType< } >; +// Warning: (ae-forgotten-export) The symbol "SidebarSearchFieldProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SidebarSearchField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SidebarSearchField: ( - props: SidebarSearchFieldProps, -) => JSX.Element; +export function SidebarSearchField(props: SidebarSearchFieldProps): JSX.Element; +// Warning: (ae-missing-release-tag) "SidebarSpace" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarSpace: React_2.ComponentType< Pick< @@ -1947,6 +1461,8 @@ export const SidebarSpace: React_2.ComponentType< } >; +// Warning: (ae-missing-release-tag) "SidebarSpacer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const SidebarSpacer: React_2.ComponentType< Pick< @@ -2214,9 +1730,14 @@ export const SidebarSpacer: React_2.ComponentType< } >; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SignInPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SignInPage: (props: Props_23) => JSX.Element; +export function SignInPage(props: Props_22): JSX.Element; +// Warning: (ae-missing-release-tag) "SignInProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SignInProviderConfig = { id: string; @@ -2225,66 +1746,80 @@ export type SignInProviderConfig = { apiRef: ApiRef; }; +// Warning: (ae-forgotten-export) The symbol "StepperProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SimpleStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SimpleStepper: ({ - children, - elevated, - onStepChange, - activeStep, -}: PropsWithChildren) => JSX.Element; +export function SimpleStepper( + props: PropsWithChildren, +): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "StepProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SimpleStepperStep" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SimpleStepperStep: ({ - title, - children, - end, - actions, - ...muiProps -}: PropsWithChildren) => JSX.Element; +export function SimpleStepperStep( + props: PropsWithChildren, +): JSX.Element; +// Warning: (ae-missing-release-tag) "StatusAborted" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StatusAborted: (props: PropsWithChildren<{}>) => JSX.Element; +export function StatusAborted(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-missing-release-tag) "StatusError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StatusError: (props: PropsWithChildren<{}>) => JSX.Element; +export function StatusError(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-missing-release-tag) "StatusOK" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StatusOK: (props: PropsWithChildren<{}>) => JSX.Element; +export function StatusOK(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-missing-release-tag) "StatusPending" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StatusPending: (props: PropsWithChildren<{}>) => JSX.Element; +export function StatusPending(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-missing-release-tag) "StatusRunning" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StatusRunning: (props: PropsWithChildren<{}>) => JSX.Element; +export function StatusRunning(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-missing-release-tag) "StatusWarning" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StatusWarning: (props: PropsWithChildren<{}>) => JSX.Element; +export function StatusWarning(props: PropsWithChildren<{}>): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "StructuredMetadataTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const StructuredMetadataTable: ({ - metadata, - dense, - options, -}: Props_16) => JSX.Element; +export function StructuredMetadataTable(props: Props_16): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "SubvalueCellProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SubvalueCell" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SubvalueCell: ({ - value, - subvalue, -}: SubvalueCellProps) => JSX.Element; +export function SubvalueCell(props: SubvalueCellProps): JSX.Element; +// Warning: (ae-forgotten-export) The symbol "SupportButtonProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SupportButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const SupportButton: ({ - title, - children, -}: SupportButtonProps) => JSX.Element; +export function SupportButton(props: SupportButtonProps): JSX.Element; +// Warning: (ae-missing-release-tag) "SupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SupportConfig = { url: string; items: SupportItem[]; }; +// Warning: (ae-missing-release-tag) "SupportItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SupportItem = { title: string; @@ -2292,12 +1827,16 @@ export type SupportItem = { links: SupportItemLink[]; }; +// Warning: (ae-missing-release-tag) "SupportItemLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type SupportItemLink = { url: string; title: string; }; +// Warning: (ae-missing-release-tag) "Tab" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type Tab = { id: string; @@ -2310,36 +1849,33 @@ export type Tab = { >; }; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "TabbedCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const TabbedCard: ({ - slackChannel, - errorBoundaryProps, - children, - title, - deepLink, - value, - onChange, -}: PropsWithChildren) => JSX.Element; +export function TabbedCard(props: PropsWithChildren): JSX.Element; +// Warning: (ae-missing-release-tag) "TabbedLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "TabbedLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const TabbedLayout: { - ({ children }: PropsWithChildren<{}>): JSX.Element; - Route: (props: SubRoute) => null; -}; +export function TabbedLayout(props: PropsWithChildren<{}>): JSX.Element; // @public (undocumented) -export function Table({ - columns, - options, - title, - subtitle, - filters, - initialState, - emptyContent, - onStateChange, - ...props -}: TableProps): JSX.Element; +export namespace TabbedLayout { + var // Warning: (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts + // + // (undocumented) + Route: (props: SubRoute) => null; +} +// Warning: (ae-missing-release-tag) "Table" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function Table(props: TableProps): JSX.Element; + +// Warning: (ae-missing-release-tag) "TableColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface TableColumn extends Column { // (undocumented) @@ -2348,12 +1884,16 @@ export interface TableColumn extends Column { width?: string; } +// Warning: (ae-missing-release-tag) "TableFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type TableFilter = { column: string; type: 'select' | 'multiple-select' | 'checkbox-tree'; }; +// Warning: (ae-missing-release-tag) "TableProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface TableProps extends MaterialTableProps { @@ -2371,6 +1911,8 @@ export interface TableProps subtitle?: string; } +// Warning: (ae-missing-release-tag) "TableState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export type TableState = { search?: string; @@ -2378,34 +1920,69 @@ export type TableState = { filters?: SelectedFilters; }; +// Warning: (ae-forgotten-export) The symbol "TabsProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "Tabs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const Tabs: ({ tabs }: TabsProps) => JSX.Element; +export function Tabs(props: TabsProps): JSX.Element; +// Warning: (ae-missing-release-tag) "TrendLine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const TrendLine: ( +export function TrendLine( props: SparklinesProps & Pick & { title?: string; }, -) => JSX.Element | null; +): JSX.Element | null; +// Warning: (ae-forgotten-export) The symbol "SetQueryParams" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "useQueryParamState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export function useQueryParamState( stateName: string, debounceTime?: number, ): [T | undefined, SetQueryParams]; +// Warning: (ae-missing-release-tag) "UserIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const UserIcon: IconComponent; +export function UserIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export function useSupportConfig(): SupportConfig; +// Warning: (ae-missing-release-tag) "WarningIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) -export const WarningIcon: IconComponent; +export function WarningIcon(props: IconComponentProps): JSX.Element; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. +// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. +// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. +// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. +// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' +// Warning: (ae-forgotten-export) The symbol "WarningProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "WarningPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public -export const WarningPanel: (props: Props_17) => JSX.Element; +export function WarningPanel(props: WarningProps): JSX.Element; + +// Warnings were encountered during analysis: +// +// src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts +// src/components/Table/Table.d.ts:15:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts +// src/layout/ErrorBoundary/ErrorBoundary.d.ts:7:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-components/package.json b/packages/core-components/package.json index b9d514d074..7ac256ad30 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.1.5", + "version": "0.4.1", "private": false, "publishConfig": { "access": "public", @@ -29,17 +29,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.5", - "@backstage/core-plugin-api": "^0.1.2", - "@backstage/errors": "^0.1.1", - "@backstage/theme": "^0.2.8", - "@material-ui/core": "^4.11.0", + "@backstage/config": "^0.1.9", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/errors": "^0.1.2", + "@backstage/theme": "^0.2.10", + "@material-table/core": "^3.1.0", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@testing-library/react-hooks": "^3.4.2", "@types/dagre": "^0.7.44", "@types/prop-types": "^15.7.3", - "@types/react": "^16.9", + "@types/react": "*", "@types/react-sparklines": "^1.7.0", "@types/react-text-truncate": "^0.14.0", "classnames": "^2.2.6", @@ -50,7 +51,6 @@ "dagre": "^0.8.5", "immer": "^9.0.1", "lodash": "^4.17.15", - "material-table": "^1.69.1", "pluralize": "^8.0.0", "prop-types": "^15.7.2", "qs": "^6.9.4", @@ -58,7 +58,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-helmet": "6.1.0", - "react-hook-form": "^6.15.4", + "react-hook-form": "^7.12.2", "react-markdown": "^5.0.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", @@ -70,20 +70,21 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.4", - "@backstage/cli": "^0.7.3", - "@backstage/test-utils": "^0.1.13", + "@backstage/core-app-api": "^0.1.12", + "@backstage/cli": "^0.7.11", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/classnames": "^2.2.9", "@types/d3-selection": "^2.0.0", - "@types/d3-shape": "^2.0.0", + "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^2.0.0", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react-helmet": "^6.1.0", + "@types/react-syntax-highlighter": "^13.5.2", "@types/zen-observable": "^0.8.0" }, "files": [ diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx index 495e2ea14c..f3e102d9a0 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx @@ -22,7 +22,7 @@ import { AlertMessage, useApi, alertApiRef } from '@backstage/core-plugin-api'; import pluralize from 'pluralize'; // TODO: improve on this and promote to a shared component for use by all apps. -export const AlertDisplay = () => { +export function AlertDisplay(_props: {}) { const [messages, setMessages] = useState>([]); const alertApi = useApi(alertApiRef); @@ -73,4 +73,4 @@ export const AlertDisplay = () => { ); -}; +} diff --git a/packages/core-components/src/components/Avatar/Avatar.tsx b/packages/core-components/src/components/Avatar/Avatar.tsx index df3c46fba7..f694365233 100644 --- a/packages/core-components/src/components/Avatar/Avatar.tsx +++ b/packages/core-components/src/components/Avatar/Avatar.tsx @@ -41,7 +41,8 @@ export type AvatarProps = { customStyles?: CSSProperties; }; -export const Avatar = ({ displayName, picture, customStyles }: AvatarProps) => { +export function Avatar(props: AvatarProps) { + const { displayName, picture, customStyles } = props; const classes = useStyles(); return ( { {displayName && extractInitials(displayName)} ); -}; +} diff --git a/packages/core-components/src/components/Button/Button.stories.tsx b/packages/core-components/src/components/Button/Button.stories.tsx index 28deb1ad33..3451a77765 100644 --- a/packages/core-components/src/components/Button/Button.stories.tsx +++ b/packages/core-components/src/components/Button/Button.stories.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentType } from 'react'; import { Button } from './Button'; -import { MemoryRouter, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api'; import { Divider, @@ -26,11 +26,7 @@ import { Typography, Button as MaterialButton, } from '@material-ui/core'; -// We don't want to export RoutingProvider from core-app-api, but it's way easier to -// use here. This hack only works in storybook stories. -// TODO: Export a nicer to user routing provider, perhaps from test-utils -// eslint-disable-next-line monorepo/no-internal-import -import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider'; +import { wrapInTestApp } from '@backstage/test-utils'; const routeRef = createRouteRef({ id: 'storybook.test-route', @@ -45,36 +41,29 @@ export default { title: 'Inputs/Button', component: Button, decorators: [ - (Story: ComponentType<{}>) => ( - <> - - 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. - + (Story: ComponentType<{}>) => + wrapInTestApp( + <> + + 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. + - + - - +

-
- -
- +
- - - - ), + +
+ , + { mountedRoutes: { '/hello': routeRef } }, + ), ], }; diff --git a/packages/core-components/src/components/Button/Button.tsx b/packages/core-components/src/components/Button/Button.tsx index 6dd593d776..e52285312b 100644 --- a/packages/core-components/src/components/Button/Button.tsx +++ b/packages/core-components/src/components/Button/Button.tsx @@ -23,10 +23,19 @@ import { Link, LinkProps } from '../Link'; type Props = MaterialButtonProps & Omit; +declare function ButtonType(props: Props): JSX.Element; + /** * Thin wrapper on top of material-ui's Button component * Makes the Button to utilise react-router */ -export const Button = React.forwardRef((props, ref) => ( +const ActualButton = React.forwardRef((props, ref) => ( -)); +)) as { (props: Props): JSX.Element }; + +// TODO(Rugvip): We use this as a workaround to make the exported type be a +// function, which makes our API reference docs much nicer. +// The first type to be exported gets priority, but it will +// be thrown away when compiling to JS. +// @ts-ignore +export { ButtonType as Button, ActualButton as Button }; diff --git a/packages/core-components/src/components/CheckboxTree/CheckboxTree.tsx b/packages/core-components/src/components/CheckboxTree/CheckboxTree.tsx index 7e7155143b..695da88192 100644 --- a/packages/core-components/src/components/CheckboxTree/CheckboxTree.tsx +++ b/packages/core-components/src/components/CheckboxTree/CheckboxTree.tsx @@ -225,13 +225,8 @@ const indexer = ( }; }, {}); -export const CheckboxTree = ({ - subCategories, - label, - selected, - onChange, - triggerReset, -}: CheckboxTreeProps) => { +export function CheckboxTree(props: CheckboxTreeProps) { + const { subCategories, label, selected, onChange, triggerReset } = props; const classes = useStyles(); const [state, dispatch] = useReducer(reducer, indexer(subCategories)); @@ -355,4 +350,4 @@ export const CheckboxTree = ({
); -}; +} diff --git a/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx b/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx index 298a8d5780..a4ffd3462b 100644 --- a/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx +++ b/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx @@ -15,8 +15,6 @@ */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; import { renderInTestApp } from '@backstage/test-utils'; import { CodeSnippet } from './CodeSnippet'; @@ -56,19 +54,4 @@ describe('', () => { expect(getByText('2')).toBeInTheDocument(); expect(getByText('3')).toBeInTheDocument(); }); - - it('copy code using button', async () => { - jest.useFakeTimers(); - document.execCommand = jest.fn(); - const { getByTitle } = await renderInTestApp( - , - ); - const button = getByTitle('Text copied to clipboard'); - fireEvent.click(button); - act(() => { - jest.runAllTimers(); - }); - expect(document.execCommand).toHaveBeenCalled(); - jest.useRealTimers(); - }); }); diff --git a/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx index a3badaad07..6417151ff4 100644 --- a/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx @@ -30,14 +30,15 @@ type Props = { customStyle?: any; }; -export const CodeSnippet = ({ - text, - language, - showLineNumbers = false, - showCopyCodeButton = false, - highlightedNumbers, - customStyle, -}: Props) => { +export const CodeSnippet = (props: Props) => { + const { + text, + language, + showLineNumbers = false, + showCopyCodeButton = false, + highlightedNumbers, + customStyle, + } = props; const theme = useTheme(); const mode = theme.palette.type === 'dark' ? dark : docco; const highlightColor = theme.palette.type === 'dark' ? '#256bf3' : '#e6ffed'; diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx index acd0fa809c..b84d3919e8 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -19,8 +19,9 @@ import { fireEvent } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { renderInTestApp } from '@backstage/test-utils'; import { CopyTextButton } from './CopyTextButton'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef, ErrorApi } from '@backstage/core-plugin-api'; +import { useCopyToClipboard } from 'react-use'; jest.mock('popper.js', () => { const PopperJS = jest.requireActual('popper.js'); @@ -33,6 +34,17 @@ jest.mock('popper.js', () => { }; }); +jest.mock('react-use', () => { + const original = jest.requireActual('react-use'); + + return { + ...original, + useCopyToClipboard: jest + .fn() + .mockImplementation(original.useCopyToClipboard), + }; +}); + const props = { text: 'mockText', tooltipDelay: 2, @@ -43,9 +55,7 @@ const apiRegistry = ApiRegistry.from([ [ errorApiRef, { - post(error) { - throw error; - }, + post: jest.fn(), error$: jest.fn(), } as ErrorApi, ], @@ -53,17 +63,22 @@ const apiRegistry = ApiRegistry.from([ describe('', () => { it('renders without exploding', async () => { - const { getByDisplayValue } = await renderInTestApp( + const { getByTitle, queryByText } = await renderInTestApp( , ); - getByDisplayValue('mockText'); + expect(getByTitle('mockTooltip')).toBeInTheDocument(); + expect(queryByText('mockTooltip')).not.toBeInTheDocument(); }); - it('displays tooltip on click', async () => { + it('displays tooltip and copy the text on click', async () => { jest.useFakeTimers(); - document.execCommand = jest.fn(); + + const spy = useCopyToClipboard as jest.Mock; + const copy = jest.fn(); + spy.mockReturnValue([{}, copy]); + const rendered = await renderInTestApp( @@ -74,8 +89,22 @@ describe('', () => { act(() => { jest.runAllTimers(); }); - expect(document.execCommand).toHaveBeenCalled(); + expect(copy).toHaveBeenCalledWith('mockText'); rendered.getByText('mockTooltip'); jest.useRealTimers(); }); + + it('reports copy errors', async () => { + const spy = useCopyToClipboard as jest.Mock; + + const error = new Error('just an error'); + spy.mockReturnValue([{ error }, jest.fn()]); + + await renderInTestApp( + + + , + ); + expect(apiRegistry.get(errorApiRef)?.post).toHaveBeenCalledWith(error); + }); }); diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx index b5bc516d1a..2d3afff1dc 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx @@ -18,7 +18,8 @@ import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { IconButton, Tooltip } from '@material-ui/core'; import CopyIcon from '@material-ui/icons/FileCopy'; import PropTypes from 'prop-types'; -import React, { MouseEventHandler, useRef, useState } from 'react'; +import React, { MouseEventHandler, useEffect, useState } from 'react'; +import { useCopyToClipboard } from 'react-use'; /** * Copy text button with visual feedback in the form of @@ -46,36 +47,29 @@ const defaultProps = { tooltipText: 'Text copied to clipboard', }; -export const CopyTextButton = (props: Props) => { +export function CopyTextButton(props: Props) { const { text, tooltipDelay, tooltipText } = { ...defaultProps, ...props, }; const errorApi = useApi(errorApiRef); - const inputRef = useRef(null); const [open, setOpen] = useState(false); + const [{ error }, copyToClipboard] = useCopyToClipboard(); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); const handleCopyClick: MouseEventHandler = e => { e.stopPropagation(); setOpen(true); - - try { - if (inputRef.current) { - inputRef.current.select(); - document.execCommand('copy'); - } - } catch (error) { - errorApi.post(error); - } + copyToClipboard(text); }; return ( <> -