diff --git a/.changeset/analytics-det-tyckte-inte-jag.md b/.changeset/analytics-det-tyckte-inte-jag.md
deleted file mode 100644
index bc486e64c4..0000000000
--- a/.changeset/analytics-det-tyckte-inte-jag.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/core-components': patch
-'@backstage/plugin-catalog-react': patch
----
-
-The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations.
diff --git a/.changeset/analytics-station-eleven.md b/.changeset/analytics-station-eleven.md
deleted file mode 100644
index e1a2366cbe..0000000000
--- a/.changeset/analytics-station-eleven.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/plugin-analytics-module-ga': patch
----
-
-Added the ability to capture and set user IDs from Backstage's `identityApi`. For full instructions on how to
-set this up, see [the User ID section of its README](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-ga#user-ids)
diff --git a/.changeset/bright-buttons-rescue.md b/.changeset/bright-buttons-rescue.md
deleted file mode 100644
index 71a6b76fa2..0000000000
--- a/.changeset/bright-buttons-rescue.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@backstage/plugin-auth-backend': minor
----
-
-**BREAKING** Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication.
-
-These changes are **required** to `packages/backend/src/plugins/auth.ts`:
-
-```diff
-export default async function createPlugin({
- logger,
- database,
- config,
- discovery,
-+ tokenManager,
-}: PluginEnvironment): Promise {
- return await createRouter({
- logger,
- config,
- database,
- discovery,
-+ tokenManager,
- });
-}
-```
-
-**BREAKING** The `CatalogIdentityClient` constructor now expects a `TokenManager` instead of a `TokenIssuer`. The `TokenManager` interface is used to generate a server token when [resolving a user's identity and membership through the catalog](https://backstage.io/docs/auth/identity-resolver). Using server tokens for these requests allows the auth-backend to bypass authorization checks when permissions are enabled for Backstage. This change will break apps that rely on the user tokens that were previously used by the client. Refer to the ["Backend-to-backend Authentication" tutorial](https://backstage.io/docs/tutorials/backend-to-backend-auth) for more information on server token usage.
diff --git a/.changeset/curly-fireants-crash.md b/.changeset/curly-fireants-crash.md
new file mode 100644
index 0000000000..db426a0fde
--- /dev/null
+++ b/.changeset/curly-fireants-crash.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+chore: bump `ansi-regex` from `5.0.1` to `6.0.1`
diff --git a/.changeset/cyan-turtles-relax.md b/.changeset/cyan-turtles-relax.md
deleted file mode 100644
index 4f7857c30c..0000000000
--- a/.changeset/cyan-turtles-relax.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-common': patch
----
-
-Adds new `catalogEntityCreatePermission` which can be imported and used when authoring a permission policy to restrict/grant a user's access to the catalog import plugin. (And the "Register Existing Component" button which navigates there).
diff --git a/.changeset/dependabot-2f11dff.md b/.changeset/dependabot-2f11dff.md
deleted file mode 100644
index f3397cad0c..0000000000
--- a/.changeset/dependabot-2f11dff.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-rollbar-backend': patch
----
-
-chore(deps): bump `camelcase-keys` from 6.2.2 to 7.0.1
diff --git a/.changeset/dependabot-9ec400d.md b/.changeset/dependabot-9ec400d.md
deleted file mode 100644
index 1fd5a85477..0000000000
--- a/.changeset/dependabot-9ec400d.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-auth-backend': patch
----
-
-chore(deps): bump `passport` from 0.4.1 to 0.5.2
diff --git a/.changeset/dependabot-f969614.md b/.changeset/dependabot-f969614.md
deleted file mode 100644
index c3c8890ec9..0000000000
--- a/.changeset/dependabot-f969614.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-backend': patch
----
-
-chore(deps): bump `prom-client` from 13.2.0 to 14.0.1
diff --git a/.changeset/early-cooks-brake.md b/.changeset/early-cooks-brake.md
deleted file mode 100644
index 3037d71ff4..0000000000
--- a/.changeset/early-cooks-brake.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Apply the fix from `0.4.16`, which is part of the `v0.65.1` release of Backstage.
diff --git a/.changeset/flat-cars-begin.md b/.changeset/flat-cars-begin.md
deleted file mode 100644
index 3a8837dc0e..0000000000
--- a/.changeset/flat-cars-begin.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core-components': patch
----
-
-Change subtitle of Header style to use palette.bursts.fontColor
diff --git a/.changeset/fresh-insects-attack.md b/.changeset/fresh-insects-attack.md
deleted file mode 100644
index 3c22df67c0..0000000000
--- a/.changeset/fresh-insects-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder': patch
----
-
-Permission the Register Existing Component button
diff --git a/.changeset/gold-queens-clap.md b/.changeset/gold-queens-clap.md
deleted file mode 100644
index 252fbb1626..0000000000
--- a/.changeset/gold-queens-clap.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core-components': patch
----
-
-Fix an issue where changes related to the `MobileSidebar` prevented scrolling pages. Additionally improve the menu of the `MobileSidebar` to not overlay the `BottomNavigation`.
diff --git a/.changeset/grumpy-teachers-remain.md b/.changeset/grumpy-teachers-remain.md
deleted file mode 100644
index 60653b084f..0000000000
--- a/.changeset/grumpy-teachers-remain.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend': patch
----
-
-Migrate from deprecated package @octokit/rest to octokit
diff --git a/.changeset/itchy-bulldogs-dance.md b/.changeset/itchy-bulldogs-dance.md
deleted file mode 100644
index a2acad4d34..0000000000
--- a/.changeset/itchy-bulldogs-dance.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-'@backstage/plugin-catalog-react': patch
----
-
-Fix bug: previously the filter would be set to "all" on page load, even if the
-`initiallySelectedFilter` on the `DefaultCatalogPage` was set to something else,
-or a different query parameter was supplied. Now, the prop and query parameters
-control the filter as expected. Additionally, after this change any filters
-which match 0 items will be disabled, and the filter will be reverted to 'all'
-if they're set on page load.
diff --git a/.changeset/nasty-pets-glow.md b/.changeset/nasty-pets-glow.md
deleted file mode 100644
index e630736261..0000000000
--- a/.changeset/nasty-pets-glow.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-org': patch
----
-
-Added `relations.memberof` filter to the catalog api call in `MemberListCard` to avoid fetching all the User entity kinds from catalog-backend.
diff --git a/.changeset/neat-mangos-study.md b/.changeset/neat-mangos-study.md
deleted file mode 100644
index 7daefcadac..0000000000
--- a/.changeset/neat-mangos-study.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Update backend-to-backend auth link in configuration file comment
diff --git a/.changeset/pre.json b/.changeset/pre.json
deleted file mode 100644
index f1f3832fce..0000000000
--- a/.changeset/pre.json
+++ /dev/null
@@ -1,150 +0,0 @@
-{
- "mode": "pre",
- "tag": "next",
- "initialVersions": {
- "example-app": "0.2.62",
- "@backstage/app-defaults": "0.1.5",
- "example-backend": "0.2.62",
- "@backstage/backend-common": "0.10.5",
- "@backstage/backend-tasks": "0.1.4",
- "@backstage/backend-test-utils": "0.1.15",
- "@backstage/catalog-client": "0.5.5",
- "@backstage/catalog-model": "0.9.10",
- "@backstage/cli": "0.13.0",
- "@backstage/cli-common": "0.1.6",
- "@backstage/codemods": "0.1.31",
- "@backstage/config": "0.1.13",
- "@backstage/config-loader": "0.9.3",
- "@backstage/core-app-api": "0.5.1",
- "@backstage/core-components": "0.8.6",
- "@backstage/core-plugin-api": "0.6.0",
- "@backstage/create-app": "0.4.15",
- "@backstage/dev-utils": "0.2.19",
- "e2e-test": "0.2.0",
- "embedded-techdocs-app": "0.2.61",
- "@backstage/errors": "0.2.0",
- "@backstage/integration": "0.7.2",
- "@backstage/integration-react": "0.1.19",
- "@backstage/search-common": "0.2.2",
- "@techdocs/cli": "0.8.11",
- "@backstage/techdocs-common": "0.11.5",
- "@backstage/test-utils": "0.2.3",
- "@backstage/theme": "0.2.14",
- "@backstage/types": "0.1.1",
- "@backstage/version-bridge": "0.1.1",
- "@backstage/plugin-airbrake": "0.1.1",
- "@backstage/plugin-allure": "0.1.12",
- "@backstage/plugin-analytics-module-ga": "0.1.7",
- "@backstage/plugin-apache-airflow": "0.1.4",
- "@backstage/plugin-api-docs": "0.7.0",
- "@backstage/plugin-app-backend": "0.3.22",
- "@backstage/plugin-auth-backend": "0.8.0",
- "@backstage/plugin-azure-devops": "0.1.12",
- "@backstage/plugin-azure-devops-backend": "0.3.1",
- "@backstage/plugin-azure-devops-common": "0.2.0",
- "@backstage/plugin-badges": "0.2.20",
- "@backstage/plugin-badges-backend": "0.1.16",
- "@backstage/plugin-bazaar": "0.1.11",
- "@backstage/plugin-bazaar-backend": "0.1.7",
- "@backstage/plugin-bitrise": "0.1.23",
- "@backstage/plugin-catalog": "0.7.10",
- "@backstage/plugin-catalog-backend": "0.21.1",
- "@backstage/plugin-catalog-backend-module-ldap": "0.3.10",
- "@backstage/plugin-catalog-backend-module-msgraph": "0.2.13",
- "@backstage/plugin-catalog-common": "0.1.1",
- "@backstage/plugin-catalog-graph": "0.2.8",
- "@backstage/plugin-catalog-graphql": "0.3.1",
- "@backstage/plugin-catalog-import": "0.7.10",
- "@backstage/plugin-catalog-react": "0.6.12",
- "@backstage/plugin-circleci": "0.2.35",
- "@backstage/plugin-cloudbuild": "0.2.33",
- "@backstage/plugin-code-coverage": "0.1.23",
- "@backstage/plugin-code-coverage-backend": "0.1.20",
- "@backstage/plugin-config-schema": "0.1.19",
- "@backstage/plugin-cost-insights": "0.11.18",
- "@backstage/plugin-explore": "0.3.27",
- "@backstage/plugin-explore-react": "0.0.11",
- "@backstage/plugin-firehydrant": "0.1.13",
- "@backstage/plugin-fossa": "0.2.28",
- "@backstage/plugin-gcp-projects": "0.3.15",
- "@backstage/plugin-git-release-manager": "0.3.9",
- "@backstage/plugin-github-actions": "0.4.33",
- "@backstage/plugin-github-deployments": "0.1.27",
- "@backstage/plugin-gitops-profiles": "0.3.14",
- "@backstage/plugin-gocd": "0.1.2",
- "@backstage/plugin-graphiql": "0.2.28",
- "@backstage/plugin-graphql-backend": "0.1.12",
- "@backstage/plugin-home": "0.4.12",
- "@backstage/plugin-ilert": "0.1.22",
- "@backstage/plugin-jenkins": "0.5.18",
- "@backstage/plugin-jenkins-backend": "0.1.11",
- "@backstage/plugin-kafka": "0.2.26",
- "@backstage/plugin-kafka-backend": "0.2.15",
- "@backstage/plugin-kubernetes": "0.5.5",
- "@backstage/plugin-kubernetes-backend": "0.4.5",
- "@backstage/plugin-kubernetes-common": "0.2.2",
- "@backstage/plugin-lighthouse": "0.2.35",
- "@backstage/plugin-newrelic": "0.3.14",
- "@backstage/plugin-newrelic-dashboard": "0.1.4",
- "@backstage/plugin-org": "0.4.0",
- "@backstage/plugin-pagerduty": "0.3.23",
- "@backstage/plugin-permission-backend": "0.4.1",
- "@backstage/plugin-permission-common": "0.4.0",
- "@backstage/plugin-permission-node": "0.4.1",
- "@backstage/plugin-permission-react": "0.3.0",
- "@backstage/plugin-proxy-backend": "0.2.16",
- "@backstage/plugin-rollbar": "0.3.24",
- "@backstage/plugin-rollbar-backend": "0.1.19",
- "@backstage/plugin-scaffolder": "0.12.0",
- "@backstage/plugin-scaffolder-backend": "0.15.22",
- "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.1.9",
- "@backstage/plugin-scaffolder-backend-module-rails": "0.2.4",
- "@backstage/plugin-scaffolder-backend-module-yeoman": "0.1.3",
- "@backstage/plugin-scaffolder-common": "0.1.3",
- "@backstage/plugin-search": "0.6.0",
- "@backstage/plugin-search-backend": "0.4.0",
- "@backstage/plugin-search-backend-module-elasticsearch": "0.0.8",
- "@backstage/plugin-search-backend-module-pg": "0.2.4",
- "@backstage/plugin-search-backend-node": "0.4.5",
- "@backstage/plugin-sentry": "0.3.34",
- "@backstage/plugin-shortcuts": "0.1.20",
- "@backstage/plugin-sonarqube": "0.2.14",
- "@backstage/plugin-splunk-on-call": "0.3.20",
- "@backstage/plugin-tech-insights": "0.1.6",
- "@backstage/plugin-tech-insights-backend": "0.2.2",
- "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.6",
- "@backstage/plugin-tech-insights-common": "0.2.1",
- "@backstage/plugin-tech-insights-node": "0.2.0",
- "@backstage/plugin-tech-radar": "0.5.3",
- "@backstage/plugin-techdocs": "0.13.1",
- "@backstage/plugin-techdocs-backend": "0.13.1",
- "@backstage/plugin-todo": "0.1.20",
- "@backstage/plugin-todo-backend": "0.1.19",
- "@backstage/plugin-user-settings": "0.3.17",
- "@backstage/plugin-xcmetrics": "0.2.16"
- },
- "changesets": [
- "analytics-station-eleven",
- "bright-buttons-rescue",
- "dependabot-2f11dff",
- "dependabot-9ec400d",
- "dependabot-f969614",
- "early-cooks-brake",
- "flat-cars-begin",
- "gold-queens-clap",
- "grumpy-teachers-remain",
- "nasty-pets-glow",
- "neat-mangos-study",
- "purple-steaks-design",
- "quick-jars-wait",
- "sharp-dragons-divide",
- "sour-chairs-double",
- "tall-rats-lie",
- "tame-ads-exercise",
- "techdocs-lets-call-the-whole-thing-off",
- "thirty-houses-juggle",
- "tiny-buses-compete",
- "witty-lamps-laugh",
- "witty-lizards-nail"
- ]
-}
diff --git a/.changeset/purple-steaks-design.md b/.changeset/purple-steaks-design.md
deleted file mode 100644
index 4857c39c73..0000000000
--- a/.changeset/purple-steaks-design.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Adds missing `/catalog-graph` route to ``.
-
-To fix this problem for a recently created app please update your `app/src/App.tsx`
-
-```diff
-+ import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
-
- ... omitted ...
-
-
- } />
-+ } />
-
-```
diff --git a/.changeset/quick-jars-wait.md b/.changeset/quick-jars-wait.md
deleted file mode 100644
index bbf6b42a89..0000000000
--- a/.changeset/quick-jars-wait.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@backstage/plugin-proxy-backend': patch
----
-
-Adds a new option `skipInvalidTargets` for the proxy `createRouter` which allows the proxy backend to be started with an invalid proxy configuration. If configured, it will simply skip the failed proxy and mount the other valid proxies.
-
-To configure it to pass by failing proxies:
-
-```
-const router = await createRouter({
- config,
- logger,
- discovery,
- skipInvalidProxies: true,
-});
-```
-
-If you would like it to fail if a proxy is configured badly:
-
-```
-const router = await createRouter({
- config,
- logger,
- discovery,
-});
-```
diff --git a/.changeset/shaggy-buckets-confess.md b/.changeset/shaggy-buckets-confess.md
new file mode 100644
index 0000000000..186d911b77
--- /dev/null
+++ b/.changeset/shaggy-buckets-confess.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-splunk-on-call': patch
+---
+
+Correct spelling of 'Acknowledge' in tooltip.
diff --git a/.changeset/sharp-dragons-divide.md b/.changeset/sharp-dragons-divide.md
deleted file mode 100644
index 398d7fb38a..0000000000
--- a/.changeset/sharp-dragons-divide.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@techdocs/cli': patch
----
-
-Use a local file dependency for embedded-techdocs-app, to ensure that it's always pulled out of the workspace
diff --git a/.changeset/silver-eagles-reply.md b/.changeset/silver-eagles-reply.md
deleted file mode 100644
index db69525768..0000000000
--- a/.changeset/silver-eagles-reply.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Permission the `catalog-import` route
-
-The following changes are **required** if you intend to add permissions to your existing app.
-
-Use the `PermissionedRoute` for `CatalogImportPage` instead of the normal `Route`:
-
-```diff
-// packages/app/src/App.tsx
-...
-+ import { PermissionedRoute } from '@backstage/plugin-permission-react';
-+ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
-
-...
-
-- } />
-+ }
-+ />
-```
diff --git a/.changeset/sour-chairs-double.md b/.changeset/sour-chairs-double.md
deleted file mode 100644
index 4f481e20e7..0000000000
--- a/.changeset/sour-chairs-double.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication.
-
-These changes are **required** to `packages/backend/src/plugins/auth.ts`:
-
-```diff
-export default async function createPlugin({
- logger,
- database,
- config,
- discovery,
-+ tokenManager,
-}: PluginEnvironment): Promise {
- return await createRouter({
- logger,
- config,
- database,
- discovery,
-+ tokenManager,
- });
-}
-```
diff --git a/.changeset/tall-rats-lie.md b/.changeset/tall-rats-lie.md
deleted file mode 100644
index ed76970df4..0000000000
--- a/.changeset/tall-rats-lie.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-pagerduty': patch
----
-
-Fix change events tab error when change events exist
diff --git a/.changeset/tame-ads-exercise.md b/.changeset/tame-ads-exercise.md
deleted file mode 100644
index a2e11393a6..0000000000
--- a/.changeset/tame-ads-exercise.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@backstage/plugin-catalog-import': minor
----
-
-Make filename, branch name and examples URLs used in catalog import customizable.
-
-Catalog backend ingestion loop can be already configured to fetch targets with custom catalog filename (other than `catalog-info.yaml`). It's now possible to customize said filename and branch name used in pull requests created by catalog import flow too. This allows organizations to further customize Backstage experience and to better reflect their branding.
-
-Filename (default: `catalog-info.yaml`) and branch name (default: `backstage-integration`) used in pull requests can be configured in `app-config.yaml` as follows:
-
-```yaml
-// app-config.yaml
-
-catalog:
- import:
- entityFilename: anvil.yaml
- pullRequestBranchName: anvil-integration
-```
-
-Following React components have also been updated to accept optional props for providing example entity and repository paths:
-
-```tsx
-
-```
-
-```tsx
-
-```
diff --git a/.changeset/techdocs-funkar-varje-gang.md b/.changeset/techdocs-funkar-varje-gang.md
deleted file mode 100644
index 727820e5f8..0000000000
--- a/.changeset/techdocs-funkar-varje-gang.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs': patch
----
-
-Fixed a bug where links to files within a TechDocs site that use the `download` attribute would result in a 404 in cases where the TechDocs backend and Backstage frontend application are on the same host.
diff --git a/.changeset/techdocs-lets-call-the-whole-thing-off.md b/.changeset/techdocs-lets-call-the-whole-thing-off.md
deleted file mode 100644
index 60cf25d65b..0000000000
--- a/.changeset/techdocs-lets-call-the-whole-thing-off.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs': patch
----
-
-Added support for documentation using the raw `` tag to point to relative resources like audio or video files.
diff --git a/.changeset/thirty-houses-juggle.md b/.changeset/thirty-houses-juggle.md
deleted file mode 100644
index c54ab5526f..0000000000
--- a/.changeset/thirty-houses-juggle.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-backend': patch
----
-
-Pass authorization token to location service inside location api routes
diff --git a/.changeset/tiny-buses-compete.md b/.changeset/tiny-buses-compete.md
deleted file mode 100644
index 2cb8d1a87f..0000000000
--- a/.changeset/tiny-buses-compete.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/core-components': patch
-'@backstage/plugin-scaffolder': patch
----
-
-Make linkTarget configurable for MarkdownContent component
diff --git a/.changeset/witty-lamps-laugh.md b/.changeset/witty-lamps-laugh.md
deleted file mode 100644
index 1ca4f4e4dc..0000000000
--- a/.changeset/witty-lamps-laugh.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Switched the `app` dependency in the backend to use a file target rather than version.
-
-To apply this change to an existing app, make the following change to `packages/backend/package.json`:
-
-```diff
- "dependencies": {
-- "app": "0.0.0",
-+ "app": "file:../app",
-```
diff --git a/.changeset/witty-lizards-nail.md b/.changeset/witty-lizards-nail.md
deleted file mode 100644
index 2ce3faa3f7..0000000000
--- a/.changeset/witty-lizards-nail.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Log warning if unable to parse yarn.lock
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a587419d50..08057d5af6 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -34,9 +34,9 @@
/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b
/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b
/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b
-/packages/embedded-techdocs-app @backstage/techdocs-core
/packages/search-common @backstage/techdocs-core
/packages/techdocs-cli @backstage/techdocs-core
+/packages/techdocs-cli-embedded-app @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/techdocs-core
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 3abe840368..8303510e77 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -28,6 +28,7 @@ Blackbox
bool
boolean
builtins
+callout
Chai
changeset
changesets
@@ -315,6 +316,7 @@ unbreak
Unconference
unmanaged
unregister
+unregistering
unregistration
untracked
upsert
@@ -338,4 +340,4 @@ Zalando
Zhou
zoomable
zsh
-Alef
\ No newline at end of file
+Alef
diff --git a/ADOPTERS.md b/ADOPTERS.md
index 64bad7439b..97bfbc255b 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -86,3 +86,6 @@
| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. |
| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. |
| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform |
+| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. |
+| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
+| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality |
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
index 936c27c7cf..3bcef61021 100644
--- a/GOVERNANCE.md
+++ b/GOVERNANCE.md
@@ -36,6 +36,25 @@ To become a maintainer you need to demonstrate the following:
If a maintainer is no longer interested or cannot perform the maintainer duties listed above, they should volunteer to be moved to emeritus status. In extreme cases this can also occur by a vote of the sponsors and maintainers per the voting process below.
+# End User Sponsors
+
+## Role of a Backstage End User Sponsor
+
+- Provide support for Backstage by removing blockers, securing funding, providing advocacy, feedback, and ensuring project continuity and long term success.
+- Assist Backstage maintainers in prioritizing upcoming roadmap items and planned work.
+- Provide neutral mediation for any disputes that arise as part of the project.
+
+## Backstage End User Sponsor Membership
+
+The End User Sponsors group comprises at most 5 people. To be eligible for membership in the group, you or the company where you work you must:
+
+- Be responsible for and end user of a production Backstage deployment of non-trivial size
+- Be active contributors to the open source project
+- Be willing and able to attend regularly-scheduled End User Sponsor meetings
+- Abide by [Backstage’s Code of Conduct](./CODE_OF_CONDUCT.md).
+
+Candidates for membership will be nominated by current Sponsor members or by Backstage maintainers. If there are more nominations than Sponsor seats remaining, existing sponsors shall vote on the candidates, and the candidates with the most votes will become Sponsors. Any ties will be broken by current Backstage sponsors.
+
# Reviewers
The project also contains a team called [@backstage/reviewers](https://github.com/orgs/backstage/teams/reviewers). This is the team of people who are the fallback in [`CODEOWNERS`](./.github/CODEOWNERS). This team will typically contain the maintainers, and a small number of additional people who are permitted to approve and merge pull requests. The purpose of this group is to offload some of the review work from the maintainers, simplifying and speeding up the review process for contributors.
diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md
index 42ab817b30..1545c34733 100644
--- a/docs/deployment/heroku.md
+++ b/docs/deployment/heroku.md
@@ -18,11 +18,10 @@ First, install the
$ heroku login
```
-Heroku runs a container registry on `registry.heroku.com`. To push Backstage
-Docker images, log in to the container registry also:
+If you have not yet created a project through the Heroku interface, you can create it through the CLI.
```shell
-$ heroku container:login
+$ heroku create
```
You _might_ also need to set your Heroku app's stack to `container`:
@@ -31,13 +30,43 @@ You _might_ also need to set your Heroku app's stack to `container`:
$ heroku stack:set container -a
```
+Configuring your `app-config.yaml`:
+
+```yaml
+app:
+ # Should be the same as backend.baseUrl when using the `app-backend` plugin
+ baseUrl: https://.herokuapp.com
+
+backend:
+ baseUrl: https://.herokuapp.com
+ listen:
+ port:
+ $env: PORT
+ # The $PORT environment variable is a feature of Heroku
+ # https://devcenter.heroku.com/articles/dynos#web-dynos
+```
+
+> Make sure your file is being copied into your container in the `Dockerfile`.
+
+Before building the Docker image, run the [backstage host build commands](https://backstage.io/docs/deployment/docker#host-build). They must be run whenever you are going to publish a new image.
+
+Heroku runs a container registry on `registry.heroku.com`. To push Backstage
+Docker images, log in to the container registry also:
+
+```shell
+$ heroku container:login
+```
+
## Push and deploy a Docker image
Now we can push a Backstage [Docker image](docker.md) to Heroku's container
registry and release it to the `web` worker:
```bash
-$ heroku container:push web -a
+$ docker image build . -f packages/backend/Dockerfile --tag registry.heroku.com//web
+
+$ docker push registry.heroku.com//web
+
$ heroku container:release web -a
```
diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md
index 71e229651a..3221b9e46a 100644
--- a/docs/features/software-catalog/api.md
+++ b/docs/features/software-catalog/api.md
@@ -21,7 +21,7 @@ section below is `/entities`, and the catalog is located at
`http://localhost:7007/api/catalog` during local development, the full URL would
be `http://localhost:7007/api/catalog/entities`. The actual URL may vary from
one organization to the other, especially in production, but is commonly your
-`backend.baseUrl` in your app config, plus `/catalog` at the end.
+`backend.baseUrl` in your app config, plus `/api/catalog` at the end.
Some or all of the endpoints may accept or require an `Authorization` header
with a `Bearer` token, which should then be the Backstage token returned by the
diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md
index 9e8a3f0563..618923289e 100644
--- a/docs/features/software-catalog/life-of-an-entity.md
+++ b/docs/features/software-catalog/life-of-an-entity.md
@@ -48,9 +48,9 @@ The details of these processes are described below.
## Ingestion
-Each catalog deployment has a number of entity providers installed. They are
+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
+that they see fit, translating those into entity objects, and notifying 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
@@ -108,7 +108,7 @@ 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
+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
@@ -185,7 +185,28 @@ happen during ingestion or processing.
## Errors
-> TODO: Describe how errors are exposed through entities
+Errors during the ingestion and processing of entities can happen in a variety
+of ways, and they may happen at a far later point in time than when they were
+registered. For example, a registered file may get deleted in the remote system,
+or the user may accidentally change the file contents in such a way that they
+cannot be parsed successfully, etc.
+
+There are two main ways that these errors are surfaced.
+
+First, the catalog backend will produce detailed logs that should contain
+sufficient information for a reader to find the causes for errors. Since these
+logs are typically not easily found by end users, this can mainly be a useful
+tool for Backstage operators who want to debug problems either with statically
+registered entities that are under their control, or to help end users find
+problems.
+
+Second, for most classes of errors, the entity itself will contain a status
+field that describes the problem. The contents of this field is shown at the top
+of your entity page in Backstage, if you have placed the corresponding error
+callout component (`EntityProcessingErrorsPanel`) there.
+
+We are still working to improve the surfacing and observability around
+processing loop errors.
## Orphaning
@@ -206,17 +227,22 @@ either, it becomes _orphaned_. The end result is as follows:
- 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.
+Orphaning can occur in several different scenarios.
+
+- If a catalog-info YAML file is moved from one place to another in the version
+ control system without updating the registration in the catalog, it will
+ effectively become orphaned "by" that registered location
+- If the user edits a corresponding parent catalog-info YAML file removing the
+ entity's entry - for example in the case of a `Location` parent entity,
+ orphaning can happen if editing or removing the `target`/`targets` lines
+ 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
@@ -239,8 +265,78 @@ provided out of the box.
## Implicit Deletion
-> TODO: Describe the process of entity providers eagerly deleting entities
+Entity providers - not processors - are subject to _eager_ deletion of entities,
+which may trigger the implicit deletion of more than just the entity you thought
+you were deleting. This concept is explained here.
+
+Recall that all entity providers manage a private "bucket" of entities, as
+described in the [External integrations](external-integrations.md) article. They
+can perform some operations on those entities, including additions, updates, and
+deletions. Entity additions/updates are subject to the regular processing loops,
+which means that bucket entities may end up forming roots of an entire graph of
+entities that are emitted by those processors as they recursively work they way
+through the bucket contents and its descendants.
+
+When a provider issues a deletion of an entity in its bucket, that entity as
+well as _the entire tree of entities processed out of it_, if any, are
+considered for immediate deletion. Note "considered" - they are deleted if and
+only if they would otherwise have become orphaned (no other parent entities
+emitting them). Since the graph of entities is not strictly a tree, multiple
+roots may actually end up indirectly referencing a node farther down in the
+graph. If that's the case, that node won't go away until all such roots go away.
+
+URLs to yaml files that you register using either the Create button or add to
+your app-config, are both handled by entity providers. That means that this
+implicit deletion mechanism comes into play in some everyday circumstances.
+Let's illustrate.
+
+Imagine that you have a monorepo, with a single `Location` entity in a
+catalog-info file at the root, and that entity points to three other
+catalog-info files in the repo with a `Component` entity in each one.
+
+```text
+/
+ feature_one/
+ catalog-info.yaml <- kind: Component
+ feature_two/
+ catalog-info.yaml <- kind: Component
+ feature_three/
+ catalog-info.yaml <- kind: Component
+ catalog-info.yaml <- kind: Location
+```
+
+If you register the root `Location` entity, the actual effect is that _five_
+entities appear in the catalog. First, one that is named `generated-`-something,
+which corresponds to the registered URL itself. That's the one that the provider
+has put in its "bucket". Then, as processing loops chug along, the `Location`
+entity you pointed to appears as a child of that, and then the three `Component`
+entities appear in turn as children of the `Location`.
+
+As an end user of the Backstage interface, you may now want to delete one of the
+three `Component` entities. You do that by visiting the three-dots menu in the
+top right of an entity view. The popup dialog that appears will inform you that
+actually this entity belongs to a certain root, and that you may want to remove
+that root instead (which corresponds to unregistering the originally registered
+URL). If you choose to do so, _all_ of the aforementioned five entities will
+actually be deleted in the same operation.
+
+If you did not want to perform this aggressive pruning, you might have instead
+chosen to remove one of the `target` rows of your `Location` catalog-info file,
+and then deleted the catalog-info file that contained the `Component` you wanted
+to get rid of. Now the catalog would be left with an orphaned component, and you
+would instead be able to use the explicit deletion (see below) to delete that
+single component.
## Explicit Deletion
-> TODO: Describe direct deletion via the catalog API
+The catalog and its REST API also permits direct deletion of individual
+entities. This makes sense to do on orphaned entities; entities that aren't
+being actively kept up to date by any parent entities. The popup interface under
+the three-dots menu option of entity views does offer this option, and the
+orphaned status can be seen in an info box at the top of the entity's overview
+page.
+
+However, if you were to try to do an explicit depletion on an entity that's
+being kept actively updated by a parent entity, it would just reappear again
+shortly thereafter when the processing loops reconsider the parent entity that's
+still in there.
diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md
index a1dffe1580..415f8cd85e 100644
--- a/docs/features/software-templates/writing-templates.md
+++ b/docs/features/software-templates/writing-templates.md
@@ -287,6 +287,79 @@ The `RepoUrlPicker` is a custom field that we provide part of the
`plugin-scaffolder`. You can provide your own custom fields by
[writing your own Custom Field Extensions](./writing-custom-field-extensions.md)
+##### Using the Users `oauth` token
+
+There's a little bit of extra magic that you get out of the box when using the
+`RepoUrlPicker` as a field input. You can provide some additional options under
+`ui:options` to allow the `RepoUrlPicker` to grab an `oauth` token for the user
+for the required `repository`.
+
+This is great for when you are wanting to create a new repository, or wanting to
+perform operations on top of an existing repository.
+
+A sample template that takes advantage of this is like so:
+
+```yaml
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ name: v1beta3-demo
+ title: Test Action template
+ description: scaffolder v1beta3 template demo
+spec:
+ owner: backstage/techdocs-core
+ type: service
+
+ parameters:
+ ...
+
+ - title: Choose a location
+ required:
+ - repoUrl
+ properties:
+ repoUrl:
+ title: Repository Location
+ type: string
+ ui:field: RepoUrlPicker
+ ui:options:
+ # Here's the option you can pass to the RepoUrlPicker
+ requestUserCredentials:
+ secretsKey: USER_OAUTH_TOKEN
+ additionalScopes:
+ github:
+ - workflow:write
+ allowedHosts:
+ - github.com
+ ...
+
+ steps:
+ ...
+
+ - id: publish
+ name: Publish
+ action: publish:github
+ input:
+ allowedHosts: ['github.com']
+ description: This is ${{ parameters.name }}
+ repoUrl: ${{ parameters.repoUrl }}
+ # here's where the secret can be used
+ token: ${{ secrets.USER_OAUTH_TOKEN }}
+
+ ...
+```
+
+You will see from above that there is an additional `requestUserCredentials`
+object that is passed to the `RepoUrlPicker`. This object defines what the
+returned `secret` should be stored as when accessing using
+`${{ secrets.secretName }}`, in this case it is `USER_OAUTH_TOKEN`. And then you
+will see that there is an additional `input` field into the `publish:github`
+action called `token`, in which you can use the `secret` like so:
+`token: ${{ secrets.USER_OAUTH_TOKEN }}`.
+
+There's also the ability to pass additional scopes when requesting the `oauth`
+token from the user, which you can do on a per-provider basis, in case your
+template can be published to multiple providers.
+
#### The Owner Picker
When the scaffolder needs to add new components to the catalog, it needs to have
diff --git a/package.json b/package.json
index 997e87035b..5c60cc41c6 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*"
},
- "version": "0.66.0-next.0",
+ "version": "0.66.0",
"dependencies": {
"@manypkg/get-packages": "^1.1.3",
"@microsoft/api-documenter": "^7.15.0",
diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md
index 588b674cfc..b58d61fcca 100644
--- a/packages/app-defaults/CHANGELOG.md
+++ b/packages/app-defaults/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/app-defaults
+## 0.1.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.8.7
+ - @backstage/core-app-api@0.5.2
+
+## 0.1.6-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.8.7-next.1
+ - @backstage/core-app-api@0.5.2-next.0
+
## 0.1.6-next.0
### Patch Changes
diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json
index 37f6061fd5..6a509a22f5 100644
--- a/packages/app-defaults/package.json
+++ b/packages/app-defaults/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
- "version": "0.1.6-next.0",
+ "version": "0.1.6",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/core-components": "^0.8.7-next.0",
- "@backstage/core-app-api": "^0.5.1",
+ "@backstage/core-components": "^0.8.7",
+ "@backstage/core-app-api": "^0.5.2",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/plugin-permission-react": "^0.3.0",
"@backstage/theme": "^0.2.14",
@@ -42,8 +42,8 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.13.1-next.0",
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/cli": "^0.13.1",
+ "@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@types/jest": "^26.0.7",
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index eefdd6f016..eb9d3cda99 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,70 @@
# example-app
+## 0.2.63
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.8.7
+ - @backstage/plugin-catalog-react@0.6.13
+ - @backstage/plugin-code-coverage@0.1.24
+ - @backstage/plugin-catalog-common@0.1.2
+ - @backstage/cli@0.13.1
+ - @backstage/plugin-scaffolder@0.12.1
+ - @backstage/integration-react@0.1.20
+ - @backstage/plugin-org@0.4.1
+ - @backstage/plugin-pagerduty@0.3.24
+ - @backstage/plugin-catalog-import@0.8.0
+ - @backstage/core-app-api@0.5.2
+ - @backstage/plugin-techdocs@0.13.2
+ - @backstage/app-defaults@0.1.6
+ - @backstage/plugin-airbrake@0.1.2
+ - @backstage/plugin-apache-airflow@0.1.5
+ - @backstage/plugin-api-docs@0.7.1
+ - @backstage/plugin-azure-devops@0.1.13
+ - @backstage/plugin-badges@0.2.21
+ - @backstage/plugin-catalog@0.7.11
+ - @backstage/plugin-catalog-graph@0.2.9
+ - @backstage/plugin-circleci@0.2.36
+ - @backstage/plugin-cloudbuild@0.2.34
+ - @backstage/plugin-cost-insights@0.11.19
+ - @backstage/plugin-explore@0.3.28
+ - @backstage/plugin-gcp-projects@0.3.16
+ - @backstage/plugin-github-actions@0.4.34
+ - @backstage/plugin-gocd@0.1.3
+ - @backstage/plugin-graphiql@0.2.29
+ - @backstage/plugin-home@0.4.13
+ - @backstage/plugin-jenkins@0.5.19
+ - @backstage/plugin-kafka@0.2.27
+ - @backstage/plugin-kubernetes@0.5.6
+ - @backstage/plugin-lighthouse@0.2.36
+ - @backstage/plugin-newrelic@0.3.15
+ - @backstage/plugin-newrelic-dashboard@0.1.5
+ - @backstage/plugin-rollbar@0.3.25
+ - @backstage/plugin-search@0.6.1
+ - @backstage/plugin-sentry@0.3.35
+ - @backstage/plugin-shortcuts@0.1.21
+ - @backstage/plugin-tech-insights@0.1.7
+ - @backstage/plugin-tech-radar@0.5.4
+ - @backstage/plugin-todo@0.1.21
+ - @backstage/plugin-user-settings@0.3.18
+
+## 0.2.63-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.8.7-next.1
+ - @backstage/plugin-catalog-react@0.6.13-next.1
+ - @backstage/plugin-code-coverage@0.1.24-next.1
+ - @backstage/plugin-catalog-common@0.1.2-next.0
+ - @backstage/cli@0.13.1-next.1
+ - @backstage/plugin-scaffolder@0.12.1-next.1
+ - @backstage/core-app-api@0.5.2-next.0
+ - @backstage/plugin-techdocs@0.13.2-next.1
+ - @backstage/plugin-catalog@0.7.11-next.1
+ - @backstage/app-defaults@0.1.6-next.1
+
## 0.2.63-next.0
### Patch Changes
diff --git a/packages/app/package.json b/packages/app/package.json
index c6e7be8c5d..3447c07059 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,56 +1,56 @@
{
"name": "example-app",
- "version": "0.2.63-next.0",
+ "version": "0.2.63",
"private": true,
"bundled": true,
"dependencies": {
- "@backstage/app-defaults": "^0.1.6-next.0",
+ "@backstage/app-defaults": "^0.1.6",
"@backstage/catalog-model": "^0.9.10",
- "@backstage/cli": "^0.13.1-next.0",
- "@backstage/core-app-api": "^0.5.1",
- "@backstage/core-components": "^0.8.7-next.0",
+ "@backstage/cli": "^0.13.1",
+ "@backstage/core-app-api": "^0.5.2",
+ "@backstage/core-components": "^0.8.7",
"@backstage/core-plugin-api": "^0.6.0",
- "@backstage/integration-react": "^0.1.20-next.0",
- "@backstage/plugin-airbrake": "^0.1.2-next.0",
- "@backstage/plugin-api-docs": "^0.7.1-next.0",
- "@backstage/plugin-azure-devops": "^0.1.13-next.0",
- "@backstage/plugin-apache-airflow": "^0.1.5-next.0",
- "@backstage/plugin-badges": "^0.2.21-next.0",
- "@backstage/plugin-catalog": "^0.7.11-next.0",
- "@backstage/plugin-catalog-common": "^0.1.1",
- "@backstage/plugin-catalog-graph": "^0.2.9-next.0",
- "@backstage/plugin-catalog-import": "^0.8.0-next.0",
- "@backstage/plugin-catalog-react": "^0.6.13-next.0",
- "@backstage/plugin-circleci": "^0.2.36-next.0",
- "@backstage/plugin-cloudbuild": "^0.2.34-next.0",
- "@backstage/plugin-code-coverage": "^0.1.24-next.0",
- "@backstage/plugin-cost-insights": "^0.11.19-next.0",
- "@backstage/plugin-explore": "^0.3.28-next.0",
- "@backstage/plugin-gcp-projects": "^0.3.16-next.0",
- "@backstage/plugin-github-actions": "^0.4.34-next.0",
- "@backstage/plugin-gocd": "^0.1.3-next.0",
- "@backstage/plugin-graphiql": "^0.2.29-next.0",
- "@backstage/plugin-home": "^0.4.13-next.0",
- "@backstage/plugin-jenkins": "^0.5.19-next.0",
- "@backstage/plugin-kafka": "^0.2.27-next.0",
- "@backstage/plugin-kubernetes": "^0.5.6-next.0",
- "@backstage/plugin-lighthouse": "^0.2.36-next.0",
- "@backstage/plugin-newrelic": "^0.3.15-next.0",
- "@backstage/plugin-newrelic-dashboard": "^0.1.5-next.0",
- "@backstage/plugin-org": "^0.4.1-next.0",
- "@backstage/plugin-pagerduty": "0.3.24-next.0",
+ "@backstage/integration-react": "^0.1.20",
+ "@backstage/plugin-airbrake": "^0.1.2",
+ "@backstage/plugin-api-docs": "^0.7.1",
+ "@backstage/plugin-azure-devops": "^0.1.13",
+ "@backstage/plugin-apache-airflow": "^0.1.5",
+ "@backstage/plugin-badges": "^0.2.21",
+ "@backstage/plugin-catalog": "^0.7.11",
+ "@backstage/plugin-catalog-common": "^0.1.2",
+ "@backstage/plugin-catalog-graph": "^0.2.9",
+ "@backstage/plugin-catalog-import": "^0.8.0",
+ "@backstage/plugin-catalog-react": "^0.6.13",
+ "@backstage/plugin-circleci": "^0.2.36",
+ "@backstage/plugin-cloudbuild": "^0.2.34",
+ "@backstage/plugin-code-coverage": "^0.1.24",
+ "@backstage/plugin-cost-insights": "^0.11.19",
+ "@backstage/plugin-explore": "^0.3.28",
+ "@backstage/plugin-gcp-projects": "^0.3.16",
+ "@backstage/plugin-github-actions": "^0.4.34",
+ "@backstage/plugin-gocd": "^0.1.3",
+ "@backstage/plugin-graphiql": "^0.2.29",
+ "@backstage/plugin-home": "^0.4.13",
+ "@backstage/plugin-jenkins": "^0.5.19",
+ "@backstage/plugin-kafka": "^0.2.27",
+ "@backstage/plugin-kubernetes": "^0.5.6",
+ "@backstage/plugin-lighthouse": "^0.2.36",
+ "@backstage/plugin-newrelic": "^0.3.15",
+ "@backstage/plugin-newrelic-dashboard": "^0.1.5",
+ "@backstage/plugin-org": "^0.4.1",
+ "@backstage/plugin-pagerduty": "0.3.24",
"@backstage/plugin-permission-react": "^0.3.0",
- "@backstage/plugin-rollbar": "^0.3.25-next.0",
- "@backstage/plugin-scaffolder": "^0.12.1-next.0",
- "@backstage/plugin-search": "^0.6.1-next.0",
- "@backstage/plugin-sentry": "^0.3.35-next.0",
- "@backstage/plugin-shortcuts": "^0.1.21-next.0",
- "@backstage/plugin-tech-radar": "^0.5.4-next.0",
- "@backstage/plugin-techdocs": "^0.13.2-next.0",
- "@backstage/plugin-todo": "^0.1.21-next.0",
- "@backstage/plugin-user-settings": "^0.3.18-next.0",
+ "@backstage/plugin-rollbar": "^0.3.25",
+ "@backstage/plugin-scaffolder": "^0.12.1",
+ "@backstage/plugin-search": "^0.6.1",
+ "@backstage/plugin-sentry": "^0.3.35",
+ "@backstage/plugin-shortcuts": "^0.1.21",
+ "@backstage/plugin-tech-radar": "^0.5.4",
+ "@backstage/plugin-techdocs": "^0.13.2",
+ "@backstage/plugin-todo": "^0.1.21",
+ "@backstage/plugin-user-settings": "^0.3.18",
"@backstage/search-common": "^0.2.2",
- "@backstage/plugin-tech-insights": "^0.1.7-next.0",
+ "@backstage/plugin-tech-insights": "^0.1.7",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -72,7 +72,7 @@
},
"devDependencies": {
"@backstage/plugin-permission-react": "^0.3.0",
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/test-utils": "^0.2.4",
"@rjsf/core": "^3.2.1",
"@testing-library/cypress": "^8.0.2",
"@testing-library/jest-dom": "^5.10.1",
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index 80e5b8f1a6..da731fa441 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/backend-common
+## 0.10.6
+
+### Patch Changes
+
+- 50d039577a: Added a `Context` type for the backend, that can propagate an abort signal, a
+ deadline, and contextual values through the call stack. The main entrypoint is
+ the `Contexts` utility class that provides a root context creator and commonly
+ used decorators.
+
+ These are marked as `@alpha` for now, and are therefore only accessible via
+ `@backstage/backend-common/alpha`.
+
+## 0.10.6-next.0
+
+### Patch Changes
+
+- 50d039577a: Added a `Context` type for the backend, that can propagate an abort signal, a
+ deadline, and contextual values through the call stack. The main entrypoint is
+ the `Contexts` utility class that provides a root context creator and commonly
+ used decorators.
+
+ These are marked as `@alpha` for now, and are therefore only accessible via
+ `@backstage/backend-common/alpha`.
+
## 0.10.5
### Patch Changes
diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md
index d63416571d..f53fb9d6e4 100644
--- a/packages/backend-common/api-report.md
+++ b/packages/backend-common/api-report.md
@@ -6,6 +6,7 @@
///
///
+import { AbortController as AbortController_2 } from 'node-abort-controller';
import { AbortSignal as AbortSignal_2 } from 'node-abort-controller';
import { AwsS3Integration } from '@backstage/integration';
import { AzureIntegration } from '@backstage/integration';
@@ -13,6 +14,7 @@ import { BitbucketIntegration } from '@backstage/integration';
import { Config } from '@backstage/config';
import cors from 'cors';
import Docker from 'dockerode';
+import { Duration } from 'luxon';
import { ErrorRequestHandler } from 'express';
import express from 'express';
import { GithubCredentialsProvider } from '@backstage/integration';
@@ -145,6 +147,29 @@ export interface ContainerRunner {
runContainer(opts: RunContainerOptions): Promise;
}
+// @alpha
+export interface Context {
+ readonly abortSignal: AbortSignal_2;
+ readonly deadline: Date | undefined;
+ value(key: string): T | undefined;
+}
+
+// @alpha
+export class Contexts {
+ static root(): Context;
+ static withAbort(
+ parentCtx: Context,
+ source: AbortController_2 | AbortSignal_2,
+ ): Context;
+ static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context;
+ static withTimeoutMillis(parentCtx: Context, timeout: number): Context;
+ static withValue(
+ parentCtx: Context,
+ key: string,
+ value: unknown | ((previous: unknown | undefined) => unknown),
+ ): Context;
+}
+
// @public @deprecated
export const createDatabase: typeof createDatabaseClient;
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index b8f56c1fbe..fdff7ca414 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,14 +1,15 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.10.5",
+ "version": "0.10.6",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
- "types": "dist/index.d.ts"
+ "types": "dist/index.d.ts",
+ "alphaTypes": "dist/index.alpha.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
@@ -21,7 +22,7 @@
],
"license": "Apache-2.0",
"scripts": {
- "build": "backstage-cli build --outputs cjs,types",
+ "build": "backstage-cli build --experimental-type-build --outputs cjs,types",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
@@ -41,6 +42,7 @@
"@types/cors": "^2.8.6",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
+ "@types/luxon": "^2.0.4",
"archiver": "^5.0.2",
"aws-sdk": "^2.840.0",
"compression": "^1.7.4",
@@ -59,6 +61,7 @@
"knex": "^0.95.1",
"lodash": "^4.17.21",
"logform": "^2.3.2",
+ "luxon": "^2.0.2",
"minimatch": "^3.0.4",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
@@ -81,8 +84,8 @@
}
},
"devDependencies": {
- "@backstage/cli": "^0.13.1-next.0",
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/cli": "^0.13.1",
+ "@backstage/test-utils": "^0.2.4",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -109,7 +112,8 @@
},
"files": [
"dist",
- "config.d.ts"
+ "config.d.ts",
+ "alpha"
],
"configSchema": "config.d.ts"
}
diff --git a/packages/backend-common/src/context/AbortContext.test.ts b/packages/backend-common/src/context/AbortContext.test.ts
new file mode 100644
index 0000000000..f47e1d4beb
--- /dev/null
+++ b/packages/backend-common/src/context/AbortContext.test.ts
@@ -0,0 +1,345 @@
+/*
+ * 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 { AbortController } from 'node-abort-controller';
+import { AbortContext } from './AbortContext';
+import { RootContext } from './RootContext';
+
+describe('AbortContext', () => {
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ describe('forTimeoutMillis', () => {
+ it('can abort on a timeout', async () => {
+ jest.useFakeTimers();
+ const timeout = 200;
+ const deadline = Date.now() + timeout;
+
+ const root = new RootContext();
+
+ const child = AbortContext.forTimeoutMillis(root, timeout);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(Math.abs(+child.deadline! - deadline)).toBeLessThan(50);
+ expect(childListener).toBeCalledTimes(0);
+
+ jest.advanceTimersByTime(timeout + 1);
+
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('results in minimum deadline when parent triggers sooner', async () => {
+ jest.useFakeTimers();
+ const parentTimeout = 200;
+ const childTimeout = 300;
+ const parentDeadline = Date.now() + parentTimeout;
+ const childDeadline = parentDeadline; // clamped
+
+ const root = new RootContext();
+
+ const parent = AbortContext.forTimeoutMillis(root, parentTimeout);
+ const parentListener = jest.fn();
+ parent.abortSignal.addEventListener('abort', parentListener);
+
+ const child = AbortContext.forTimeoutMillis(parent, childTimeout);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50);
+ expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(0);
+
+ jest.advanceTimersByTime(parentTimeout + 1);
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(1);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('results in minimum deadline when child triggers sooner', async () => {
+ jest.useFakeTimers();
+ const parentTimeout = 300;
+ const childTimeout = 200;
+ const parentDeadline = Date.now() + parentTimeout;
+ const childDeadline = Date.now() + childTimeout;
+
+ const root = new RootContext();
+
+ const parent = AbortContext.forTimeoutMillis(root, parentTimeout);
+ const parentListener = jest.fn();
+ parent.abortSignal.addEventListener('abort', parentListener);
+
+ const child = AbortContext.forTimeoutMillis(parent, childTimeout);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50);
+ expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(0);
+
+ jest.advanceTimersByTime(childTimeout + 1);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(1);
+
+ jest.advanceTimersByTime(parentTimeout - childTimeout + 1);
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(1);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('child carries over parent signal state if parent was already aborted and had no deadline', async () => {
+ jest.useFakeTimers();
+ const childTimeout = 200;
+ const childDeadline = Date.now() + childTimeout;
+
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forSignal(root, parentController.signal);
+
+ parentController.abort();
+
+ const child = AbortContext.forTimeoutMillis(parent, childTimeout);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+ expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
+
+ jest.advanceTimersByTime(childTimeout + 1);
+
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0); // still
+ });
+
+ it('child carries over parent signal state if parent was already aborted and had a deadline', async () => {
+ jest.useFakeTimers();
+ const first = new RootContext();
+
+ const secondController = new AbortController();
+ const second = AbortContext.forSignal(first, secondController.signal);
+ secondController.abort();
+
+ const third = AbortContext.forTimeoutMillis(second, 200);
+ const fourth = AbortContext.forTimeoutMillis(third, 300);
+
+ expect(third.abortSignal.aborted).toBe(true);
+ expect(fourth.abortSignal.aborted).toBe(true);
+ expect(Math.abs(+fourth.deadline! - Date.now() - 200)).toBeLessThan(50);
+ });
+ });
+
+ describe('forController', () => {
+ it('signals child when parent is aborted', () => {
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forController(root, parentController);
+ const parentListener = jest.fn();
+ parent.abortSignal.addEventListener('abort', parentListener);
+
+ const childController = new AbortController();
+ const child = AbortContext.forController(parent, childController);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(0);
+
+ parentController.abort();
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(1);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('does not signal parent when child is aborted', async () => {
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forController(root, parentController);
+ const parentListener = jest.fn();
+ parent.abortSignal.addEventListener('abort', parentListener);
+
+ const childController = new AbortController();
+ const child = AbortContext.forController(parent, childController);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(0);
+
+ childController.abort();
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('child carries over parent signal state if parent was already aborted', async () => {
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forController(root, parentController);
+
+ parentController.abort();
+
+ const childController = new AbortController();
+ const child = AbortContext.forController(parent, childController);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+
+ childController.abort();
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+ });
+
+ it('child carries over given signal state if it was already aborted', async () => {
+ const root = new RootContext();
+
+ const childController = new AbortController();
+ childController.abort();
+
+ const child = AbortContext.forController(root, childController);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+ });
+ });
+
+ describe('forSignal', () => {
+ it('signals child when parent is aborted', async () => {
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forSignal(root, parentController.signal);
+ const parentListener = jest.fn();
+ parent.abortSignal.addEventListener('abort', parentListener);
+
+ const childController = new AbortController();
+ const child = AbortContext.forSignal(parent, childController.signal);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(0);
+
+ parentController.abort();
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(1);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('does not signal parent when child is aborted', async () => {
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forSignal(root, parentController.signal);
+ const parentListener = jest.fn();
+ parent.abortSignal.addEventListener('abort', parentListener);
+
+ const childController = new AbortController();
+ const child = AbortContext.forSignal(parent, childController.signal);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(false);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(0);
+
+ childController.abort();
+
+ expect(parent.abortSignal.aborted).toBe(false);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(parentListener).toBeCalledTimes(0);
+ expect(childListener).toBeCalledTimes(1);
+ });
+
+ it('child carries over parent signal state if parent was already aborted', async () => {
+ const root = new RootContext();
+
+ const parentController = new AbortController();
+ const parent = AbortContext.forSignal(root, parentController.signal);
+
+ parentController.abort();
+
+ const childController = new AbortController();
+ const child = AbortContext.forSignal(parent, childController.signal);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+
+ childController.abort();
+
+ expect(parent.abortSignal.aborted).toBe(true);
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+ });
+
+ it('child carries over given signal state if it was already aborted', async () => {
+ const root = new RootContext();
+
+ const childController = new AbortController();
+ childController.abort();
+
+ const child = AbortContext.forSignal(root, childController.signal);
+ const childListener = jest.fn();
+ child.abortSignal.addEventListener('abort', childListener);
+
+ expect(child.abortSignal.aborted).toBe(true);
+ expect(childListener).toBeCalledTimes(0);
+ });
+ });
+});
diff --git a/packages/backend-common/src/context/AbortContext.ts b/packages/backend-common/src/context/AbortContext.ts
new file mode 100644
index 0000000000..8a119358a6
--- /dev/null
+++ b/packages/backend-common/src/context/AbortContext.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 { AbortController, AbortSignal } from 'node-abort-controller';
+import { Context } from './types';
+
+/**
+ * A context that implements various abort related functionality.
+ */
+export class AbortContext implements Context {
+ /**
+ * Abort either when the parent aborts, or after the given timeout has
+ * expired.
+ *
+ * @param ctx - The parent context
+ * @param timeout - A timeout value, in milliseconds
+ * @returns A new context
+ */
+ static forTimeoutMillis(ctx: Context, timeout: number): Context {
+ const desiredDeadline = new Date(Date.now() + timeout);
+ const actualDeadline =
+ ctx.deadline && ctx.deadline < desiredDeadline
+ ? ctx.deadline
+ : desiredDeadline;
+
+ if (ctx.abortSignal.aborted) {
+ if (ctx.deadline && desiredDeadline === actualDeadline) {
+ return ctx;
+ }
+ return new AbortContext(ctx, ctx.abortSignal, actualDeadline);
+ }
+
+ const controller = new AbortController();
+ const timeoutHandle = setTimeout(abort, timeout);
+ ctx.abortSignal.addEventListener('abort', abort);
+
+ function abort() {
+ ctx.abortSignal.removeEventListener('abort', abort);
+ clearTimeout(timeoutHandle!);
+ controller.abort();
+ }
+
+ return new AbortContext(ctx, controller.signal, actualDeadline);
+ }
+
+ /**
+ * Abort either when the parent aborts, or when the given controller is
+ * triggered.
+ *
+ * @remarks
+ *
+ * If you have access to the controller, this function is more efficient than
+ * {@link AbortContext#forSignal}.
+ *
+ * @param ctx - The parent context
+ * @param controller - An abort controller
+ * @returns A new context
+ */
+ static forController(ctx: Context, controller: AbortController): Context {
+ // Already aborted context / signal are fine to reuse as-is
+ if (ctx.abortSignal.aborted) {
+ return ctx;
+ } else if (controller.signal.aborted) {
+ return new AbortContext(ctx, controller.signal, ctx.deadline);
+ }
+
+ function abort() {
+ ctx.abortSignal.removeEventListener('abort', abort);
+ controller.abort();
+ }
+
+ ctx.abortSignal.addEventListener('abort', abort);
+
+ return new AbortContext(ctx, controller.signal, ctx.deadline);
+ }
+
+ /**
+ * Abort either when the parent aborts, or when the given signal is triggered.
+ *
+ * @remarks
+ *
+ * If you have access to the controller and not just the signal,
+ * {@link AbortContext#forController} is slightly more efficient to use.
+ *
+ * @param ctx - The parent context
+ * @param signal - An abort signal
+ * @returns A new context
+ */
+ static forSignal(ctx: Context, signal: AbortSignal): Context {
+ // Already aborted context / signal are fine to reuse as-is
+ if (ctx.abortSignal.aborted) {
+ return ctx;
+ } else if (signal.aborted) {
+ return new AbortContext(ctx, signal, ctx.deadline);
+ }
+
+ const controller = new AbortController();
+
+ function abort() {
+ ctx.abortSignal.removeEventListener('abort', abort);
+ signal.removeEventListener('abort', abort);
+ controller.abort();
+ }
+
+ ctx.abortSignal.addEventListener('abort', abort);
+ signal.addEventListener('abort', abort);
+
+ return new AbortContext(ctx, controller.signal, ctx.deadline);
+ }
+
+ private constructor(
+ private readonly parent: Context,
+ readonly abortSignal: AbortSignal,
+ readonly deadline: Date | undefined,
+ ) {}
+
+ value(key: string): T | undefined {
+ return this.parent.value(key);
+ }
+}
diff --git a/packages/backend-common/src/context/Contexts.test.ts b/packages/backend-common/src/context/Contexts.test.ts
new file mode 100644
index 0000000000..cbd61d20a9
--- /dev/null
+++ b/packages/backend-common/src/context/Contexts.test.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 { Duration } from 'luxon';
+import { AbortController } from 'node-abort-controller';
+import { Contexts } from './Contexts';
+
+describe('Contexts', () => {
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ describe('root', () => {
+ it('can create a root', () => {
+ const ctx = Contexts.root();
+ expect(ctx.abortSignal).toBeDefined();
+ expect(ctx.deadline).toBeUndefined();
+ });
+ });
+
+ describe('setAbort', () => {
+ it('works for controllers', () => {
+ const controller = new AbortController();
+ const parent = Contexts.root();
+ const child = Contexts.withAbort(parent, controller);
+ expect(child.abortSignal.aborted).toBe(false);
+ controller.abort();
+ expect(child.abortSignal.aborted).toBe(true);
+ });
+
+ it('works for signals', () => {
+ const controller = new AbortController();
+ const parent = Contexts.root();
+ const child = Contexts.withAbort(parent, controller.signal);
+ expect(child.abortSignal.aborted).toBe(false);
+ controller.abort();
+ expect(child.abortSignal.aborted).toBe(true);
+ });
+ });
+
+ describe('setTimeoutDuration', () => {
+ it('works', () => {
+ jest.useFakeTimers();
+ const parent = Contexts.root();
+ const child = Contexts.withTimeoutDuration(
+ parent,
+ Duration.fromMillis(200),
+ );
+ expect(child.abortSignal.aborted).toBe(false);
+ jest.advanceTimersByTime(100);
+ expect(child.abortSignal.aborted).toBe(false);
+ jest.advanceTimersByTime(101);
+ expect(child.abortSignal.aborted).toBe(true);
+ });
+ });
+
+ describe('setTimeoutMillis', () => {
+ it('works', () => {
+ jest.useFakeTimers();
+ const parent = Contexts.root();
+ const child = Contexts.withTimeoutMillis(parent, 200);
+ expect(child.abortSignal.aborted).toBe(false);
+ jest.advanceTimersByTime(100);
+ expect(child.abortSignal.aborted).toBe(false);
+ jest.advanceTimersByTime(101);
+ expect(child.abortSignal.aborted).toBe(true);
+ });
+ });
+
+ describe('setValue', () => {
+ it('works', () => {
+ const parent = Contexts.root();
+ const child = Contexts.withValue(parent, 'k', 'v');
+ expect(child.value('k')).toBe('v');
+ });
+ });
+});
diff --git a/packages/backend-common/src/context/Contexts.ts b/packages/backend-common/src/context/Contexts.ts
new file mode 100644
index 0000000000..e22a76f673
--- /dev/null
+++ b/packages/backend-common/src/context/Contexts.ts
@@ -0,0 +1,114 @@
+/*
+ * 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 { Duration } from 'luxon';
+import { AbortController, AbortSignal } from 'node-abort-controller';
+import { AbortContext } from './AbortContext';
+import { RootContext } from './RootContext';
+import { Context } from './types';
+import { ValueContext } from './ValueContext';
+
+/**
+ * Common context decorators.
+ *
+ * @alpha
+ */
+export class Contexts {
+ /**
+ * Creates a root context.
+ *
+ * @remarks
+ *
+ * This should normally only be called near the root of an application. The
+ * created context is meant to be passed down into deeper levels, which may or
+ * may not make derived contexts out of it.
+ */
+ static root(): Context {
+ return new RootContext();
+ }
+
+ /**
+ * Creates a derived context, which signals to abort operations either when
+ * any parent context signals, or when the given source is aborted.
+ *
+ * @remarks
+ *
+ * If the parent context was already aborted, then it is returned as-is.
+ *
+ * If the given source was already aborted, then a new already-aborted context
+ * is returned.
+ *
+ * @param parentCtx - A parent context that shall be used as a base
+ * @param source - An abort controller or signal that you intend to perhaps
+ * trigger at some later point in time.
+ * @returns A new {@link Context}
+ */
+ static withAbort(
+ parentCtx: Context,
+ source: AbortController | AbortSignal,
+ ): Context {
+ return 'aborted' in source
+ ? AbortContext.forSignal(parentCtx, source)
+ : AbortContext.forController(parentCtx, source);
+ }
+
+ /**
+ * Creates a derived context, which signals to abort operations either when
+ * any parent context signals, or when the given amount of time has passed.
+ * This may affect the deadline.
+ *
+ * @param parentCtx - A parent context that shall be used as a base
+ * @param timeout - The duration of time, after which the derived context will
+ * signal to abort.
+ * @returns A new {@link Context}
+ */
+ static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context {
+ return AbortContext.forTimeoutMillis(parentCtx, timeout.as('milliseconds'));
+ }
+
+ /**
+ * Creates a derived context, which signals to abort operations either when
+ * any parent context signals, or when the given amount of time has passed.
+ * This may affect the deadline.
+ *
+ * @param parentCtx - A parent context that shall be used as a base
+ * @param timeout - The number of milliseconds, after which the derived
+ * context will signal to abort.
+ * @returns A new {@link Context}
+ */
+ static withTimeoutMillis(parentCtx: Context, timeout: number): Context {
+ return AbortContext.forTimeoutMillis(parentCtx, timeout);
+ }
+
+ /**
+ * Creates a derived context, which has a specific key-value pair set as well
+ * as all key-value pairs that were set in the original context.
+ *
+ * @param parentCtx - A parent context that shall be used as a base
+ * @param key - The key of the value to set
+ * @param value - The value, or a function that accepts the previous value (or
+ * undefined if not set yet) and computes the new value
+ * @returns A new {@link Context}
+ */
+ static withValue(
+ parentCtx: Context,
+ key: string,
+ value: unknown | ((previous: unknown | undefined) => unknown),
+ ): Context {
+ const v = typeof value === 'function' ? value(parentCtx.value(key)) : value;
+ return ValueContext.forConstantValue(parentCtx, key, v);
+ }
+}
diff --git a/packages/backend-common/src/context/RootContext.test.ts b/packages/backend-common/src/context/RootContext.test.ts
new file mode 100644
index 0000000000..395786e3dc
--- /dev/null
+++ b/packages/backend-common/src/context/RootContext.test.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { RootContext } from './RootContext';
+
+describe('RootContext', () => {
+ it('returns empty values', async () => {
+ const ctx = new RootContext();
+ expect(ctx.abortSignal).toBeDefined();
+ expect(ctx.abortSignal.aborted).toBe(false);
+ expect(ctx.deadline).toBeUndefined();
+ expect(ctx.value('a')).toBeUndefined();
+ });
+});
diff --git a/packages/backend-common/src/context/RootContext.ts b/packages/backend-common/src/context/RootContext.ts
new file mode 100644
index 0000000000..61962380a4
--- /dev/null
+++ b/packages/backend-common/src/context/RootContext.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { AbortSignal } from 'node-abort-controller';
+import { Context } from './types';
+
+/**
+ * Since the root context can never abort, and since nobody is every meant to
+ * dispatch events through it, we can use a static dummy instance for
+ * efficiency.
+ */
+const dummyAbortSignal: AbortSignal = Object.freeze({
+ aborted: false,
+ addEventListener() {},
+ removeEventListener() {},
+ dispatchEvent() {
+ return true;
+ },
+ onabort: null,
+});
+
+/**
+ * An empty root context.
+ */
+export class RootContext implements Context {
+ readonly abortSignal = dummyAbortSignal;
+ readonly deadline = undefined;
+
+ value(_key: string): T | undefined {
+ return undefined;
+ }
+}
diff --git a/packages/backend-common/src/context/ValueContext.test.ts b/packages/backend-common/src/context/ValueContext.test.ts
new file mode 100644
index 0000000000..6a216d4548
--- /dev/null
+++ b/packages/backend-common/src/context/ValueContext.test.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { RootContext } from './RootContext';
+import { ValueContext } from './ValueContext';
+
+describe('ValueContext', () => {
+ it('returns its own values, or delegates to the parent', async () => {
+ const root = new RootContext();
+ const a = ValueContext.forConstantValue(root, 'a', 1);
+ const b = ValueContext.forConstantValue(a, 'x', 2);
+ const c = ValueContext.forConstantValue(b, 'a', 3);
+ const d = ValueContext.forConstantValue(c, 'b', 4);
+
+ expect(a.value('a')).toBe(1);
+ expect(a.value('b')).toBeUndefined();
+ expect(a.value('x')).toBeUndefined();
+
+ expect(b.value('a')).toBe(1);
+ expect(b.value('b')).toBeUndefined();
+ expect(b.value('x')).toBe(2);
+
+ expect(c.value('a')).toBe(3);
+ expect(c.value('b')).toBeUndefined();
+ expect(c.value('x')).toBe(2);
+
+ expect(d.value('a')).toBe(3);
+ expect(d.value('b')).toBe(4);
+ expect(d.value('x')).toBe(2);
+ });
+});
diff --git a/packages/backend-common/src/context/ValueContext.ts b/packages/backend-common/src/context/ValueContext.ts
new file mode 100644
index 0000000000..441aaec984
--- /dev/null
+++ b/packages/backend-common/src/context/ValueContext.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { AbortSignal } from 'node-abort-controller';
+import { Context } from './types';
+
+/**
+ * A context that just holds a single value, and delegates the rest to its
+ * parent.
+ */
+export class ValueContext implements Context {
+ static forConstantValue(ctx: Context, key: string, value: unknown): Context {
+ return new ValueContext(ctx, key, value);
+ }
+
+ constructor(
+ private readonly _parent: Context,
+ private readonly _key: string,
+ private readonly _value: unknown,
+ ) {}
+
+ get abortSignal(): AbortSignal {
+ return this._parent.abortSignal;
+ }
+
+ get deadline(): Date | undefined {
+ return this._parent.deadline;
+ }
+
+ value(key: string): T | undefined {
+ return key === this._key ? (this._value as T) : this._parent.value(key);
+ }
+}
diff --git a/packages/backend-common/src/context/index.ts b/packages/backend-common/src/context/index.ts
new file mode 100644
index 0000000000..37a6e29c8c
--- /dev/null
+++ b/packages/backend-common/src/context/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+export { Contexts } from './Contexts';
+export type { Context } from './types';
diff --git a/packages/backend-common/src/context/types.ts b/packages/backend-common/src/context/types.ts
new file mode 100644
index 0000000000..664745bbf6
--- /dev/null
+++ b/packages/backend-common/src/context/types.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { AbortSignal } from 'node-abort-controller';
+
+/**
+ * A context that is meant to be passed as a ctx variable down the call chain,
+ * to pass along scoped information and abort signals.
+ *
+ * @alpha
+ */
+export interface Context {
+ /**
+ * Returns an abort signal that triggers when the current context or any of
+ * its parents signal for it.
+ */
+ readonly abortSignal: AbortSignal;
+
+ /**
+ * The point in time when the current context shall time out and abort, if
+ * applicable.
+ */
+ readonly deadline: Date | undefined;
+
+ /**
+ * Attempts to get a stored value by key from the context.
+ *
+ * @param key - The key of the value to get
+ * @returns The associated value, or undefined if not set
+ */
+ value(key: string): T | undefined;
+}
diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts
index e430ddc1a4..238e58b2a8 100644
--- a/packages/backend-common/src/index.ts
+++ b/packages/backend-common/src/index.ts
@@ -22,6 +22,7 @@
export * from './cache';
export { loadBackendConfig } from './config';
+export * from './context';
export * from './database';
export * from './discovery';
export * from './hot';
diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md
index cfaf40a6d6..b13142a7cf 100644
--- a/packages/backend-tasks/CHANGELOG.md
+++ b/packages/backend-tasks/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/backend-tasks
+## 0.1.5
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.10.6
+
+## 0.1.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.10.6-next.0
+
## 0.1.4
### Patch Changes
diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json
index 838f38dbd2..a060e5e99c 100644
--- a/packages/backend-tasks/package.json
+++ b/packages/backend-tasks/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
- "version": "0.1.4",
+ "version": "0.1.5",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.10.5",
+ "@backstage/backend-common": "^0.10.6",
"@backstage/config": "^0.1.13",
"@backstage/errors": "^0.2.0",
"@backstage/types": "^0.1.1",
@@ -43,8 +43,8 @@
"zod": "^3.9.5"
},
"devDependencies": {
- "@backstage/backend-test-utils": "^0.1.16-next.0",
- "@backstage/cli": "^0.13.1-next.0",
+ "@backstage/backend-test-utils": "^0.1.16",
+ "@backstage/cli": "^0.13.1",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md
index 006ea6d634..0ecc9afb4f 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/backend-test-utils
+## 0.1.16
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli@0.13.1
+ - @backstage/backend-common@0.10.6
+
+## 0.1.16-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli@0.13.1-next.1
+ - @backstage/backend-common@0.10.6-next.0
+
## 0.1.16-next.0
### Patch Changes
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index 7aabe3e08d..b6db1694c3 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.16-next.0",
+ "version": "0.1.16",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,8 +30,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.10.5",
- "@backstage/cli": "^0.13.1-next.0",
+ "@backstage/backend-common": "^0.10.6",
+ "@backstage/cli": "^0.13.1",
"@backstage/config": "^0.1.13",
"knex": "^0.95.1",
"mysql2": "^2.2.5",
@@ -41,7 +41,7 @@
"uuid": "^8.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.13.1-next.0",
+ "@backstage/cli": "^0.13.1",
"jest": "^26.0.1"
},
"files": [
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index 8882a9203e..40bfad7ddf 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,69 @@
# example-backend
+## 0.2.63
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-auth-backend@0.9.0
+ - @backstage/plugin-rollbar-backend@0.1.20
+ - @backstage/plugin-catalog-backend@0.21.2
+ - @backstage/plugin-scaffolder-backend@0.15.23
+ - @backstage/plugin-proxy-backend@0.2.17
+ - @backstage/backend-common@0.10.6
+ - example-app@0.2.63
+ - @backstage/backend-tasks@0.1.5
+ - @backstage/plugin-app-backend@0.3.23
+ - @backstage/plugin-azure-devops-backend@0.3.2
+ - @backstage/plugin-badges-backend@0.1.17
+ - @backstage/plugin-code-coverage-backend@0.1.21
+ - @backstage/plugin-graphql-backend@0.1.13
+ - @backstage/plugin-jenkins-backend@0.1.12
+ - @backstage/plugin-kafka-backend@0.2.16
+ - @backstage/plugin-kubernetes-backend@0.4.6
+ - @backstage/plugin-permission-backend@0.4.2
+ - @backstage/plugin-permission-node@0.4.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.2.5
+ - @backstage/plugin-search-backend@0.4.1
+ - @backstage/plugin-search-backend-module-pg@0.2.5
+ - @backstage/plugin-tech-insights-backend@0.2.3
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.7
+ - @backstage/plugin-tech-insights-node@0.2.1
+ - @backstage/plugin-techdocs-backend@0.13.2
+ - @backstage/plugin-todo-backend@0.1.20
+
+## 0.2.63-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-auth-backend@0.9.0-next.1
+ - @backstage/backend-common@0.10.6-next.0
+ - example-app@0.2.63-next.1
+ - @backstage/plugin-catalog-backend@0.21.2-next.1
+ - @backstage/plugin-techdocs-backend@0.13.2-next.0
+ - @backstage/backend-tasks@0.1.5-next.0
+ - @backstage/plugin-app-backend@0.3.23-next.0
+ - @backstage/plugin-azure-devops-backend@0.3.2-next.0
+ - @backstage/plugin-badges-backend@0.1.17-next.0
+ - @backstage/plugin-code-coverage-backend@0.1.21-next.0
+ - @backstage/plugin-graphql-backend@0.1.13-next.0
+ - @backstage/plugin-jenkins-backend@0.1.12-next.0
+ - @backstage/plugin-kafka-backend@0.2.16-next.0
+ - @backstage/plugin-kubernetes-backend@0.4.6-next.0
+ - @backstage/plugin-permission-backend@0.4.2-next.1
+ - @backstage/plugin-permission-node@0.4.2-next.1
+ - @backstage/plugin-proxy-backend@0.2.17-next.1
+ - @backstage/plugin-rollbar-backend@0.1.20-next.1
+ - @backstage/plugin-scaffolder-backend@0.15.23-next.1
+ - @backstage/plugin-scaffolder-backend-module-rails@0.2.5-next.1
+ - @backstage/plugin-search-backend@0.4.1-next.1
+ - @backstage/plugin-search-backend-module-pg@0.2.5-next.0
+ - @backstage/plugin-tech-insights-backend@0.2.3-next.0
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.7-next.0
+ - @backstage/plugin-tech-insights-node@0.2.1-next.0
+ - @backstage/plugin-todo-backend@0.1.20-next.0
+
## 0.2.63-next.0
### Patch Changes
diff --git a/packages/backend/package.json b/packages/backend/package.json
index c9e1c9d7c3..67c959376c 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.63-next.0",
+ "version": "0.2.63",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -24,38 +24,38 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
- "@backstage/backend-common": "^0.10.5",
- "@backstage/backend-tasks": "^0.1.4",
+ "@backstage/backend-common": "^0.10.6",
+ "@backstage/backend-tasks": "^0.1.5",
"@backstage/catalog-client": "^0.5.5",
"@backstage/catalog-model": "^0.9.10",
"@backstage/config": "^0.1.13",
"@backstage/integration": "^0.7.2",
- "@backstage/plugin-app-backend": "^0.3.22",
- "@backstage/plugin-auth-backend": "^0.9.0-next.0",
- "@backstage/plugin-azure-devops-backend": "^0.3.1",
- "@backstage/plugin-badges-backend": "^0.1.16",
- "@backstage/plugin-catalog-backend": "^0.21.2-next.0",
- "@backstage/plugin-code-coverage-backend": "^0.1.20",
- "@backstage/plugin-graphql-backend": "^0.1.12",
- "@backstage/plugin-jenkins-backend": "^0.1.11",
- "@backstage/plugin-kubernetes-backend": "^0.4.5",
- "@backstage/plugin-kafka-backend": "^0.2.15",
- "@backstage/plugin-permission-backend": "^0.4.2-next.0",
+ "@backstage/plugin-app-backend": "^0.3.23",
+ "@backstage/plugin-auth-backend": "^0.9.0",
+ "@backstage/plugin-azure-devops-backend": "^0.3.2",
+ "@backstage/plugin-badges-backend": "^0.1.17",
+ "@backstage/plugin-catalog-backend": "^0.21.2",
+ "@backstage/plugin-code-coverage-backend": "^0.1.21",
+ "@backstage/plugin-graphql-backend": "^0.1.13",
+ "@backstage/plugin-jenkins-backend": "^0.1.12",
+ "@backstage/plugin-kubernetes-backend": "^0.4.6",
+ "@backstage/plugin-kafka-backend": "^0.2.16",
+ "@backstage/plugin-permission-backend": "^0.4.2",
"@backstage/plugin-permission-common": "^0.4.0",
- "@backstage/plugin-permission-node": "^0.4.2-next.0",
- "@backstage/plugin-proxy-backend": "^0.2.17-next.0",
- "@backstage/plugin-rollbar-backend": "^0.1.20-next.0",
- "@backstage/plugin-scaffolder-backend": "^0.15.23-next.0",
- "@backstage/plugin-scaffolder-backend-module-rails": "^0.2.5-next.0",
- "@backstage/plugin-search-backend": "^0.4.1-next.0",
+ "@backstage/plugin-permission-node": "^0.4.2",
+ "@backstage/plugin-proxy-backend": "^0.2.17",
+ "@backstage/plugin-rollbar-backend": "^0.1.20",
+ "@backstage/plugin-scaffolder-backend": "^0.15.23",
+ "@backstage/plugin-scaffolder-backend-module-rails": "^0.2.5",
+ "@backstage/plugin-search-backend": "^0.4.1",
"@backstage/plugin-search-backend-node": "^0.4.5",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.8",
- "@backstage/plugin-search-backend-module-pg": "^0.2.4",
- "@backstage/plugin-techdocs-backend": "^0.13.1",
- "@backstage/plugin-tech-insights-backend": "^0.2.2",
- "@backstage/plugin-tech-insights-node": "^0.2.0",
- "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.6",
- "@backstage/plugin-todo-backend": "^0.1.19",
+ "@backstage/plugin-search-backend-module-pg": "^0.2.5",
+ "@backstage/plugin-techdocs-backend": "^0.13.2",
+ "@backstage/plugin-tech-insights-backend": "^0.2.3",
+ "@backstage/plugin-tech-insights-node": "^0.2.1",
+ "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.7",
+ "@backstage/plugin-todo-backend": "^0.1.20",
"@gitbeaker/node": "^35.1.0",
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^11.0.1",
@@ -72,7 +72,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.13.1-next.0",
+ "@backstage/cli": "^0.13.1",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index bcea1a68ee..a74e3ba0a7 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/cli
+## 0.13.1
+
+### Patch Changes
+
+- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0
+- 80f510caee: Log warning if unable to parse yarn.lock
+
+## 0.13.1-next.1
+
+### Patch Changes
+
+- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0
+
## 0.13.1-next.0
### Patch Changes
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 824e8e5efc..aad08a21eb 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.13.1-next.0",
+ "version": "0.13.1",
"private": false,
"publishConfig": {
"access": "public"
@@ -78,7 +78,7 @@
"glob": "^7.1.7",
"handlebars": "^4.7.3",
"html-webpack-plugin": "^5.3.1",
- "inquirer": "^7.0.4",
+ "inquirer": "^8.2.0",
"jest": "^26.0.1",
"jest-css-modules": "^2.1.0",
"json-schema": "^0.4.0",
@@ -115,13 +115,13 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/backend-common": "^0.10.5",
+ "@backstage/backend-common": "^0.10.6",
"@backstage/config": "^0.1.13",
- "@backstage/core-components": "^0.8.7-next.0",
+ "@backstage/core-components": "^0.8.7",
"@backstage/core-plugin-api": "^0.6.0",
- "@backstage/core-app-api": "^0.5.1",
- "@backstage/dev-utils": "^0.2.20-next.0",
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/core-app-api": "^0.5.2",
+ "@backstage/dev-utils": "^0.2.20",
+ "@backstage/test-utils": "^0.2.4",
"@backstage/theme": "^0.2.14",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md
index ec2fbfee4e..d14d46efd8 100644
--- a/packages/codemods/CHANGELOG.md
+++ b/packages/codemods/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/codemods
+## 0.1.32
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.8.7
+ - @backstage/core-app-api@0.5.2
+
+## 0.1.32-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.8.7-next.1
+ - @backstage/core-app-api@0.5.2-next.0
+
## 0.1.32-next.0
### Patch Changes
diff --git a/packages/codemods/package.json b/packages/codemods/package.json
index 9cd2cb8567..4ce5d9700d 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.32-next.0",
+ "version": "0.1.32",
"private": false,
"publishConfig": {
"access": "public",
diff --git a/packages/config/package.json b/packages/config/package.json
index 44fb7ab286..02c63af2b5 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -34,7 +34,7 @@
"lodash": "^4.17.21"
},
"devDependencies": {
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/test-utils": "^0.2.4-next.0",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md
index 06692a0b46..cc115a7da2 100644
--- a/packages/core-app-api/CHANGELOG.md
+++ b/packages/core-app-api/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/core-app-api
+## 0.5.2
+
+### Patch Changes
+
+- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`.
+
+ > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change.
+
+## 0.5.2-next.0
+
+### Patch Changes
+
+- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`.
+
+ > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change.
+
## 0.5.1
### Patch Changes
diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md
index ba2ed8cb62..3b53813615 100644
--- a/packages/core-app-api/api-report.md
+++ b/packages/core-app-api/api-report.md
@@ -35,6 +35,7 @@ import { FeatureFlag } from '@backstage/core-plugin-api';
import { FeatureFlagsApi } from '@backstage/core-plugin-api';
import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
+import { githubAuthApiRef } from '@backstage/core-plugin-api';
import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
@@ -379,25 +380,11 @@ export type FlatRoutesProps = {
};
// @public
-export class GithubAuth implements OAuthApi, SessionApi {
- // (undocumented)
- static create(options: OAuthApiCreateOptions): GithubAuth;
- // (undocumented)
- getAccessToken(scope?: string, options?: AuthRequestOptions): Promise;
- // (undocumented)
- getBackstageIdentity(
- options?: AuthRequestOptions,
- ): Promise;
- // (undocumented)
- getProfile(options?: AuthRequestOptions): Promise;
+export class GithubAuth {
// (undocumented)
+ static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T;
+ // @deprecated (undocumented)
static normalizeScope(scope?: string): Set;
- // (undocumented)
- sessionState$(): Observable;
- // (undocumented)
- signIn(): Promise;
- // (undocumented)
- signOut(): Promise;
}
// @public @deprecated
diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json
index b60008a1a8..a546e29134 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.5.1",
+ "version": "0.5.2",
"private": false,
"publishConfig": {
"access": "public",
@@ -45,8 +45,8 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.13.1-next.0",
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/cli": "^0.13.1",
+ "@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^7.0.2",
diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts
index 8bcd4cb7a5..ea7b4ac688 100644
--- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts
@@ -14,16 +14,33 @@
* limitations under the License.
*/
+import { UrlPatternDiscovery } from '../../DiscoveryApi';
+import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import GithubAuth from './GithubAuth';
-describe('GithubAuth', () => {
- it('should get access token', async () => {
- const getSession = jest
- .fn()
- .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } });
- const githubAuth = new (GithubAuth as any)({ getSession }) as GithubAuth;
+const getSession = jest.fn();
- expect(await githubAuth.getAccessToken()).toBe('access-token');
- expect(getSession).toBeCalledTimes(1);
+jest.mock('../../../../lib/AuthSessionManager', () => ({
+ ...(jest.requireActual('../../../../lib/AuthSessionManager') as any),
+ RefreshingAuthSessionManager: class {
+ getSession = getSession;
+ },
+}));
+
+describe('GithubAuth', () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should forward access token request to session manager', async () => {
+ const githubAuth = GithubAuth.create({
+ oauthRequestApi: new MockOAuthApi(),
+ discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
+ });
+
+ githubAuth.getAccessToken('repo');
+ expect(getSession).toHaveBeenCalledWith({
+ scopes: new Set(['repo']),
+ });
});
});
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 dc2c15bc16..b0af4c7ade 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
@@ -14,35 +14,9 @@
* limitations under the License.
*/
-import {
- AuthRequestOptions,
- BackstageIdentityResponse,
- OAuthApi,
- ProfileInfo,
- SessionApi,
- SessionState,
-} from '@backstage/core-plugin-api';
-import { Observable } from '@backstage/types';
-import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
-import {
- AuthSessionStore,
- RefreshingAuthSessionManager,
- StaticAuthSessionManager,
-} from '../../../../lib/AuthSessionManager';
-import { OptionalRefreshSessionManagerMux } from '../../../../lib/AuthSessionManager/OptionalRefreshSessionManagerMux';
-import { SessionManager } from '../../../../lib/AuthSessionManager/types';
+import { githubAuthApiRef } from '@backstage/core-plugin-api';
+import { OAuth2 } from '../oauth2';
import { OAuthApiCreateOptions } from '../types';
-import { GithubSession, githubSessionSchema } from './types';
-
-export type GithubAuthResponse = {
- providerInfo: {
- accessToken: string;
- scope: string;
- expiresInSeconds?: number;
- };
- profile: ProfileInfo;
- backstageIdentity: BackstageIdentityResponse;
-};
const DEFAULT_PROVIDER = {
id: 'github',
@@ -55,8 +29,8 @@ const DEFAULT_PROVIDER = {
*
* @public
*/
-export default class GithubAuth implements OAuthApi, SessionApi {
- static create(options: OAuthApiCreateOptions) {
+export default class GithubAuth {
+ static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T {
const {
discoveryApi,
environment = 'development',
@@ -65,96 +39,18 @@ export default class GithubAuth implements OAuthApi, SessionApi {
defaultScopes = ['read:user'],
} = options;
- const connector = new DefaultAuthConnector({
+ return OAuth2.create({
discoveryApi,
- environment,
+ oauthRequestApi,
provider,
- oauthRequestApi: oauthRequestApi,
- sessionTransform(res: GithubAuthResponse): GithubSession {
- return {
- ...res,
- providerInfo: {
- accessToken: res.providerInfo.accessToken,
- scopes: GithubAuth.normalizeScope(res.providerInfo.scope),
- expiresAt: res.providerInfo.expiresInSeconds
- ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)
- : undefined,
- },
- };
- },
+ environment,
+ defaultScopes,
});
-
- 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 staticSessionManager = new AuthSessionStore({
- manager: new StaticAuthSessionManager({
- connector,
- defaultScopes: new Set(defaultScopes),
- sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
- }),
- storageKey: `${provider.id}Session`,
- schema: githubSessionSchema,
- sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
- });
-
- const sessionManagerMux = new OptionalRefreshSessionManagerMux({
- refreshingSessionManager,
- staticSessionManager,
- sessionCanRefresh: session =>
- session.providerInfo.expiresAt !== undefined,
- });
-
- return new GithubAuth(sessionManagerMux);
- }
-
- private constructor(
- private readonly sessionManager: SessionManager,
- ) {}
-
- async signIn() {
- await this.getAccessToken();
- }
-
- async signOut() {
- await this.sessionManager.removeSession();
- }
-
- sessionState$(): Observable {
- return this.sessionManager.sessionState$();
- }
-
- async getAccessToken(scope?: string, options?: AuthRequestOptions) {
- const session = await this.sessionManager.getSession({
- ...options,
- scopes: GithubAuth.normalizeScope(scope),
- });
- return session?.providerInfo.accessToken ?? '';
- }
-
- async getBackstageIdentity(
- options: AuthRequestOptions = {},
- ): Promise {
- const session = await this.sessionManager.getSession(options);
- return session?.backstageIdentity;
- }
-
- async getProfile(options: AuthRequestOptions = {}) {
- const session = await this.sessionManager.getSession(options);
- return session?.profile;
}
+ /**
+ * @deprecated This method is deprecated and will be removed in a future release.
+ */
static normalizeScope(scope?: string): Set {
if (!scope) {
return new Set();
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index a22b029abd..27cf543205 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/core-components
+## 0.8.7
+
+### Patch Changes
+
+- f7257dff6f: The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations.
+- 4c773ed25c: Change subtitle of Header style to use palette.bursts.fontColor
+- f465b63b7f: Fix an issue where changes related to the `MobileSidebar` prevented scrolling pages. Additionally improve the menu of the `MobileSidebar` to not overlay the `BottomNavigation`.
+- 064e750a50: Adding hover message to the Gauge and an info icon to the GaugeCard.
+- a681cb9c2f: Make linkTarget configurable for MarkdownContent component
+
+## 0.8.7-next.1
+
+### Patch Changes
+
+- f7257dff6f: The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations.
+
## 0.8.7-next.0
### Patch Changes
diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md
index 47ca3d3cf8..6365b4f9d7 100644
--- a/packages/core-components/api-report.md
+++ b/packages/core-components/api-report.md
@@ -384,7 +384,12 @@ export function GaugeCard(props: Props_10): JSX.Element;
export type GaugeCardClassKey = 'root';
// @public (undocumented)
-export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown';
+export type GaugeClassKey =
+ | 'root'
+ | 'overlay'
+ | 'description'
+ | 'circle'
+ | 'colorUnknown';
// @public (undocumented)
export type GaugeProps = {
@@ -393,6 +398,7 @@ export type GaugeProps = {
inverse?: boolean;
unit?: string;
max?: number;
+ description?: ReactNode;
getColor?: GaugePropsGetColor;
};
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index ed4df67a89..a6e9962abe 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.8.7-next.0",
+ "version": "0.8.7",
"private": false,
"publishConfig": {
"access": "public",
@@ -39,7 +39,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@types/react-sparklines": "^1.7.0",
"@types/react-text-truncate": "^0.14.0",
- "ansi-regex": "^5.0.1",
+ "ansi-regex": "^6.0.1",
"classnames": "^2.2.6",
"d3-selection": "^3.0.0",
"d3-shape": "^3.0.0",
@@ -73,9 +73,9 @@
"react-dom": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
- "@backstage/core-app-api": "^0.5.1",
- "@backstage/cli": "^0.13.1-next.0",
- "@backstage/test-utils": "^0.2.3",
+ "@backstage/core-app-api": "^0.5.2",
+ "@backstage/cli": "^0.13.1",
+ "@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^7.0.2",
diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx
index 0336ebc990..11f977b387 100644
--- a/packages/core-components/src/components/ProgressBars/Gauge.tsx
+++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx
@@ -17,10 +17,15 @@
import { BackstagePalette, BackstageTheme } from '@backstage/theme';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import { Circle } from 'rc-progress';
-import React from 'react';
+import React, { ReactNode, useEffect, useState } from 'react';
/** @public */
-export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown';
+export type GaugeClassKey =
+ | 'root'
+ | 'overlay'
+ | 'description'
+ | 'circle'
+ | 'colorUnknown';
const useStyles = makeStyles(
theme => ({
@@ -37,6 +42,15 @@ const useStyles = makeStyles(
fontWeight: 'bold',
color: theme.palette.textContrast,
},
+ description: {
+ fontSize: '100%',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ position: 'absolute',
+ wordBreak: 'break-all',
+ display: 'inline-block',
+ },
circle: {
width: '80%',
transform: 'translate(10%, 0)',
@@ -53,6 +67,7 @@ export type GaugeProps = {
inverse?: boolean;
unit?: string;
max?: number;
+ description?: ReactNode;
getColor?: GaugePropsGetColor;
};
@@ -104,10 +119,11 @@ export const getProgressColor: GaugePropsGetColor = ({
*/
export function Gauge(props: GaugeProps) {
+ const [hoverRef, setHoverRef] = useState(null);
const { getColor = getProgressColor } = props;
const classes = useStyles(props);
const { palette } = useTheme();
- const { value, fractional, inverse, unit, max } = {
+ const { value, fractional, inverse, unit, max, description } = {
...defaultGaugeProps,
...props,
};
@@ -115,8 +131,28 @@ export function Gauge(props: GaugeProps) {
const asPercentage = fractional ? Math.round(value * max) : value;
const asActual = max !== 100 ? Math.round(value) : asPercentage;
+ const [isHovering, setIsHovering] = useState(false);
+
+ useEffect(() => {
+ const node = hoverRef;
+ const handleMouseOver = () => setIsHovering(true);
+ const handleMouseOut = () => setIsHovering(false);
+ if (node && description) {
+ node.addEventListener('mouseenter', handleMouseOver);
+ node.addEventListener('mouseleave', handleMouseOut);
+
+ return () => {
+ node.removeEventListener('mouseenter', handleMouseOver);
+ node.removeEventListener('mouseleave', handleMouseOut);
+ };
+ }
+ return () => {
+ setIsHovering(false);
+ };
+ }, [description, hoverRef]);
+
return (
-