Merge remote-tracking branch 'upstream/master' into airbrake-api

# Conflicts:
#	ADOPTERS.md
#	plugins/airbrake/package.json
This commit is contained in:
Karan Shah
2022-02-04 11:02:20 +00:00
354 changed files with 5265 additions and 2144 deletions
@@ -1,6 +0,0 @@
---
'@backstage/core-components': patch
'@backstage/plugin-catalog-react': patch
---
The `<Link />` 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.
-6
View File
@@ -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)
-27
View File
@@ -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<Router> {
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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
chore: bump `ansi-regex` from `5.0.1` to `6.0.1`
-5
View File
@@ -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).
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-rollbar-backend': patch
---
chore(deps): bump `camelcase-keys` from 6.2.2 to 7.0.1
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
chore(deps): bump `passport` from 0.4.1 to 0.5.2
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
chore(deps): bump `prom-client` from 13.2.0 to 14.0.1
-5
View File
@@ -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.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-components': patch
---
Change subtitle of Header style to use palette.bursts.fontColor
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder': patch
---
Permission the Register Existing Component button
-5
View File
@@ -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`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Migrate from deprecated package @octokit/rest to octokit
-10
View File
@@ -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.
-5
View File
@@ -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.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/create-app': patch
---
Update backend-to-backend auth link in configuration file comment
-150
View File
@@ -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"
]
}
-18
View File
@@ -1,18 +0,0 @@
---
'@backstage/create-app': patch
---
Adds missing `/catalog-graph` route to `<CatalogGraphPage/>`.
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 ...
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
+ <Route path="/catalog-graph" element={<CatalogGraphPage />} />
</FlatRoutes>
```
-26
View File
@@ -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,
});
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-splunk-on-call': patch
---
Correct spelling of 'Acknowledge' in tooltip.
-5
View File
@@ -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
-25
View File
@@ -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';
...
- <Route path="/catalog-import" element={<CatalogImportPage />} />
+ <PermissionedRoute
+ path="/catalog-import"
+ permission={catalogEntityCreatePermission}
+ element={<CatalogImportPage />}
+ />
```
-25
View File
@@ -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<Router> {
return await createRouter({
logger,
config,
database,
discovery,
+ tokenManager,
});
}
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-pagerduty': patch
---
Fix change events tab error when change events exist
-34
View File
@@ -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
<StepInitAnalyzeUrl
...
exampleLocationUrl="https://github.com/acme-corp/our-awesome-api/blob/main/anvil.yaml"
/>
```
```tsx
<ImportInfoCard
exampleLocationUrl="https://github.com/acme-corp/our-awesome-api/blob/main/anvil.yaml"
exampleRepositoryUrl="https://github.com/acme-corp/our-awesome-api"
/>
```
-5
View File
@@ -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.
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Added support for documentation using the raw `<source>` tag to point to relative resources like audio or video files.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Pass authorization token to location service inside location api routes
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-components': patch
'@backstage/plugin-scaffolder': patch
---
Make linkTarget configurable for MarkdownContent component
-13
View File
@@ -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",
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Log warning if unable to parse yarn.lock
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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
Alef
+3
View File
@@ -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 |
+19
View File
@@ -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 [Backstages 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.
+33 -4
View File
@@ -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 <your-app>
```
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 <your-app>
```
Configuring your `app-config.yaml`:
```yaml
app:
# Should be the same as backend.baseUrl when using the `app-backend` plugin
baseUrl: https://<your-app>.herokuapp.com
backend:
baseUrl: https://<your-app>.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 <your-app>
$ docker image build . -f packages/backend/Dockerfile --tag registry.heroku.com/<your-app>/web
$ docker push registry.heroku.com/<your-app>/web
$ heroku container:release web -a <your-app>
```
+1 -1
View File
@@ -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
@@ -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.
@@ -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
+1 -1
View File
@@ -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",
+16
View File
@@ -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
+5 -5
View File
@@ -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",
+65
View File
@@ -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
+45 -45
View File
@@ -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",
+24
View File
@@ -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
+25
View File
@@ -6,6 +6,7 @@
/// <reference types="node" />
/// <reference types="webpack-env" />
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<void>;
}
// @alpha
export interface Context {
readonly abortSignal: AbortSignal_2;
readonly deadline: Date | undefined;
value<T = unknown>(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;
+10 -6
View File
@@ -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"
}
@@ -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);
});
});
});
@@ -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<T = unknown>(key: string): T | undefined {
return this.parent.value(key);
}
}
@@ -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');
});
});
});
@@ -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);
}
}
@@ -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();
});
});
@@ -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<T = unknown>(_key: string): T | undefined {
return undefined;
}
}
@@ -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);
});
});
@@ -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<T = unknown>(key: string): T | undefined {
return key === this._key ? (this._value as T) : this._parent.value(key);
}
}
@@ -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';
@@ -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<T = unknown>(key: string): T | undefined;
}
+1
View File
@@ -22,6 +22,7 @@
export * from './cache';
export { loadBackendConfig } from './config';
export * from './context';
export * from './database';
export * from './discovery';
export * from './hot';
+14
View File
@@ -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
+4 -4
View File
@@ -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"
},
+16
View File
@@ -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
+4 -4
View File
@@ -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": [
+64
View File
@@ -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
+27 -27
View File
@@ -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"
+13
View File
@@ -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
+7 -7
View File
@@ -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",
+16
View File
@@ -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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
},
+16
View File
@@ -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
+4 -17
View File
@@ -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<string>;
// (undocumented)
getBackstageIdentity(
options?: AuthRequestOptions,
): Promise<BackstageIdentityResponse | undefined>;
// (undocumented)
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
export class GithubAuth {
// (undocumented)
static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T;
// @deprecated (undocumented)
static normalizeScope(scope?: string): Set<string>;
// (undocumented)
sessionState$(): Observable<SessionState>;
// (undocumented)
signIn(): Promise<void>;
// (undocumented)
signOut(): Promise<void>;
}
// @public @deprecated
+3 -3
View File
@@ -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",
@@ -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']),
});
});
});
@@ -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<GithubSession>({
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<GithubSession>,
) {}
async signIn() {
await this.getAccessToken();
}
async signOut() {
await this.sessionManager.removeSession();
}
sessionState$(): Observable<SessionState> {
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<BackstageIdentityResponse | undefined> {
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<string> {
if (!scope) {
return new Set();
+16
View File
@@ -1,5 +1,21 @@
# @backstage/core-components
## 0.8.7
### Patch Changes
- f7257dff6f: The `<Link />` 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 `<Link />` 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
+7 -1
View File
@@ -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;
};
+5 -5
View File
@@ -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",
@@ -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<BackstageTheme>(
theme => ({
@@ -37,6 +42,15 @@ const useStyles = makeStyles<BackstageTheme>(
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<HTMLDivElement | null>(null);
const { getColor = getProgressColor } = props;
const classes = useStyles(props);
const { palette } = useTheme<BackstageTheme>();
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 (
<div className={classes.root}>
<div ref={setHoverRef} className={classes.root}>
<Circle
strokeLinecap="butt"
percent={asPercentage}
@@ -125,9 +161,13 @@ export function Gauge(props: GaugeProps) {
strokeColor={getColor({ palette, value: asActual, inverse, max })}
className={classes.circle}
/>
<div className={classes.overlay}>
{isNaN(value) ? 'N/A' : `${asActual}${unit}`}
</div>
{description && isHovering ? (
<div className={classes.description}>{description}</div>
) : (
<div className={classes.overlay}>
{isNaN(value) ? 'N/A' : `${asActual}${unit}`}
</div>
)}
</div>
);
}
@@ -18,6 +18,8 @@ import React, { PropsWithChildren } from 'react';
import { GaugeCard } from './GaugeCard';
import Grid from '@material-ui/core/Grid';
import { MemoryRouter } from 'react-router';
import Tooltip from '@material-ui/core/Tooltip';
import Info from '@material-ui/icons/Info';
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
@@ -118,3 +120,79 @@ export const StaticColor = () => (
</Grid>
</Wrapper>
);
export const InfoMessage = () => (
<Wrapper>
<Grid item>
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.3}
icon={
<Tooltip title="Info Message" arrow>
<Info style={{ float: 'right' }} />
</Tooltip>
}
/>
</Grid>
<Grid item>
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.57}
icon={
<Tooltip title="Info Message" arrow>
<Info style={{ float: 'right' }} />
</Tooltip>
}
/>
</Grid>
<Grid item>
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.89}
icon={
<Tooltip title="Info Message" arrow>
<Info style={{ float: 'right' }} />
</Tooltip>
}
/>
</Grid>
<Grid item>
<GaugeCard
title="Progress"
subheader="With a subheader"
inverse
progress={0.2}
icon={
<Tooltip title="Info Message" arrow>
<Info style={{ float: 'right' }} />
</Tooltip>
}
/>
</Grid>
</Wrapper>
);
export const HoverMessage = () => (
<Wrapper>
<Grid item>
<GaugeCard title="Progress" progress={0.3} description="Hover Message" />
</Grid>
<Grid item>
<GaugeCard title="Progress" progress={0.57} description="Hover Message" />
</Grid>
<Grid item>
<GaugeCard title="Progress" progress={0.89} description="Hover Message" />
</Grid>
<Grid item>
<GaugeCard
title="Progress"
inverse
progress={0.2}
description="Hover Message"
/>
</Grid>
</Wrapper>
);
@@ -15,7 +15,7 @@
*/
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import React, { ReactNode } from 'react';
import { BottomLinkProps } from '../../layout/BottomLink';
import { InfoCard, InfoCardVariants } from '../../layout/InfoCard';
import { Gauge, GaugePropsGetColor } from './Gauge';
@@ -26,6 +26,8 @@ type Props = {
variant?: InfoCardVariants;
/** Progress in % specified as decimal, e.g. "0.23" */
progress: number;
description?: ReactNode;
icon?: ReactNode;
inverse?: boolean;
deepLink?: BottomLinkProps;
getColor?: GaugePropsGetColor;
@@ -52,11 +54,21 @@ const useStyles = makeStyles(
*/
export function GaugeCard(props: Props) {
const classes = useStyles(props);
const { title, subheader, progress, inverse, deepLink, variant, getColor } =
props;
const {
title,
subheader,
progress,
inverse,
deepLink,
description,
icon,
variant,
getColor,
} = props;
const gaugeProps = {
inverse,
description,
getColor,
value: progress,
};
@@ -68,6 +80,7 @@ export function GaugeCard(props: Props) {
subheader={subheader}
deepLink={deepLink}
variant={variant}
icon={icon}
>
<Gauge {...gaugeProps} />
</InfoCard>
@@ -55,6 +55,9 @@ const useStyles = makeStyles(
headerAvatar: {},
headerAction: {},
headerContent: {},
subheader: {
display: 'flex',
},
}),
{ name: 'BackstageInfoCard' },
);
@@ -134,6 +137,7 @@ type Props = {
children?: ReactNode;
headerStyle?: object;
headerProps?: CardHeaderProps;
icon?: ReactNode;
action?: ReactNode;
actionsClassName?: string;
actions?: ReactNode;
@@ -162,6 +166,7 @@ export function InfoCard(props: Props): JSX.Element {
children,
headerStyle,
headerProps,
icon,
action,
actionsClassName,
actions,
@@ -194,6 +199,15 @@ export function InfoCard(props: Props): JSX.Element {
});
}
const cardSubTitle = () => {
return (
<div className={classes.headerSubheader}>
{subheader && <div className={classes.subheader}>{subheader}</div>}
{icon}
</div>
);
};
const errProps: ErrorBoundaryProps =
errorBoundaryProps || (slackChannel ? { slackChannel } : {});
@@ -211,7 +225,7 @@ export function InfoCard(props: Props): JSX.Element {
content: classes.headerContent,
}}
title={title}
subheader={subheader}
subheader={cardSubTitle()}
action={action}
style={{ ...headerStyle }}
titleTypographyProps={titleTypographyProps}
+3 -3
View File
@@ -43,9 +43,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/core-app-api": "^0.5.1",
"@backstage/test-utils": "^0.2.3",
"@backstage/cli": "^0.13.1-next.1",
"@backstage/core-app-api": "^0.5.2-next.0",
"@backstage/test-utils": "^0.2.4-next.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^7.0.2",
+104 -1
View File
@@ -1,5 +1,108 @@
# @backstage/create-app
## 0.4.18
### Patch Changes
- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0
- f27f5197e2: Apply the fix from `0.4.16`, which is part of the `v0.65.1` release of Backstage.
- 2687029a67: Update backend-to-backend auth link in configuration file comment
- 24ef62048c: Adds missing `/catalog-graph` route to `<CatalogGraphPage/>`.
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 ...
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
+ <Route path="/catalog-graph" element={<CatalogGraphPage />} />
</FlatRoutes>
```
- ba59832aed: 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';
...
- <Route path="/catalog-import" element={<CatalogImportPage />} />
+ <PermissionedRoute
+ path="/catalog-import"
+ permission={catalogEntityCreatePermission}
+ element={<CatalogImportPage />}
+ />
```
- cef64b1561: 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<Router> {
return await createRouter({
logger,
config,
database,
discovery,
+ tokenManager,
});
}
```
- e39d88bd84: 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",
```
## 0.4.18-next.1
### Patch Changes
- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0
- ba59832aed: 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';
...
- <Route path="/catalog-import" element={<CatalogImportPage />} />
+ <PermissionedRoute
+ path="/catalog-import"
+ permission={catalogEntityCreatePermission}
+ element={<CatalogImportPage />}
+ />
```
## 0.4.18-next.0
### Patch Changes
@@ -102,7 +205,7 @@
To make this change to an existing app:
Add `@backstage/catalog-graph-plugin` as a `dependency` in `packages/app/package.json`
Add `@backstage/plugin-catalog-graph` as a `dependency` in `packages/app/package.json`
Apply the following changes to the `packages/app/src/components/catalog/EntityPage.tsx` file:
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
"version": "0.4.18-next.0",
"version": "0.4.18",
"private": false,
"publishConfig": {
"access": "public"
@@ -35,7 +35,7 @@
"commander": "^6.1.0",
"fs-extra": "9.1.0",
"handlebars": "^4.7.3",
"inquirer": "^7.0.4",
"inquirer": "^8.2.0",
"ora": "^5.3.0",
"recursive-readdir": "^2.2.2"
},
+23
View File
@@ -1,5 +1,28 @@
# @backstage/dev-utils
## 0.2.20
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.7
- @backstage/plugin-catalog-react@0.6.13
- @backstage/integration-react@0.1.20
- @backstage/core-app-api@0.5.2
- @backstage/app-defaults@0.1.6
- @backstage/test-utils@0.2.4
## 0.2.20-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.7-next.1
- @backstage/plugin-catalog-react@0.6.13-next.1
- @backstage/core-app-api@0.5.2-next.0
- @backstage/app-defaults@0.1.6-next.1
- @backstage/test-utils@0.2.4-next.0
## 0.2.20-next.0
### Patch Changes
+8 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.2.20-next.0",
"version": "0.2.20",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,14 +29,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/app-defaults": "^0.1.6-next.0",
"@backstage/core-app-api": "^0.5.1",
"@backstage/core-components": "^0.8.7-next.0",
"@backstage/app-defaults": "^0.1.6",
"@backstage/core-app-api": "^0.5.2",
"@backstage/core-components": "^0.8.7",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/catalog-model": "^0.9.10",
"@backstage/integration-react": "^0.1.20-next.0",
"@backstage/plugin-catalog-react": "^0.6.13-next.0",
"@backstage/test-utils": "^0.2.3",
"@backstage/integration-react": "^0.1.20",
"@backstage/plugin-catalog-react": "^0.6.13",
"@backstage/test-utils": "^0.2.4",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -55,7 +55,7 @@
"react-dom": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/cli": "^0.13.1",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
+8
View File
@@ -1,5 +1,13 @@
# @backstage/integration-react
## 0.1.20
### Patch Changes
- cee44ad289: Added the ability to collect users `oauth` token from the `RepoUrlPicker` for use in the template manifest
- Updated dependencies
- @backstage/core-components@0.8.7
## 0.1.20-next.0
### Patch Changes
+7
View File
@@ -29,6 +29,7 @@ export class ScmAuth implements ScmAuthApi {
ProfileInfoApi &
BackstageIdentityApi &
SessionApi;
bitbucket: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi;
}
>;
static forAuthApi(
@@ -82,6 +83,12 @@ export const scmAuthApiRef: ApiRef<ScmAuthApi>;
export interface ScmAuthTokenOptions extends AuthRequestOptions {
additionalScope?: {
repoWrite?: boolean;
customScopes?: {
github?: string[];
azure?: string[];
bitbucket?: string[];
gitlab?: string[];
};
};
url: string;
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
"version": "0.1.20-next.0",
"version": "0.1.20",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -22,7 +22,7 @@
},
"dependencies": {
"@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/integration": "^0.7.2",
"@backstage/theme": "^0.2.14",
@@ -35,9 +35,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/dev-utils": "^0.2.20-next.0",
"@backstage/test-utils": "^0.2.3",
"@backstage/cli": "^0.13.1",
"@backstage/dev-utils": "^0.2.20",
"@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
@@ -141,6 +141,58 @@ describe('ScmAuth', () => {
});
});
it('should support additional provided scopes from the caller', async () => {
const mockAuthApi = {
getAccessToken: async (scopes: string[]) => {
return scopes.join(' ');
},
};
const githubAuth = ScmAuth.forGithub(mockAuthApi);
await expect(
githubAuth.getCredentials({
url: 'http://example.com',
additionalScope: {
customScopes: { github: ['org:read', 'workflow:write'] },
},
}),
).resolves.toMatchObject({
token: 'repo read:org read:user org:read workflow:write',
});
const gitlabAuth = ScmAuth.forGitlab(mockAuthApi);
await expect(
gitlabAuth.getCredentials({
url: 'http://example.com',
additionalScope: { customScopes: { gitlab: ['write_repository'] } },
}),
).resolves.toMatchObject({
token: 'read_user read_api read_repository write_repository',
});
const azureAuth = ScmAuth.forAzure(mockAuthApi);
await expect(
azureAuth.getCredentials({
url: 'http://example.com',
additionalScope: { customScopes: { azure: ['vso.org'] } },
}),
).resolves.toMatchObject({
token: 'vso.build vso.code vso.graph vso.project vso.profile vso.org',
});
const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi);
await expect(
bitbucketAuth.getCredentials({
url: 'http://example.com',
additionalScope: {
customScopes: { bitbucket: ['snippet:write', 'issue:write'] },
},
}),
).resolves.toMatchObject({
token: 'account team pullrequest snippet issue snippet:write issue:write',
});
});
it('should handle host option', () => {
const mockAuthApi = {
getAccessToken: jest.fn(),
+41 -8
View File
@@ -15,6 +15,7 @@
*/
import {
bitbucketAuthApiRef,
createApiFactory,
githubAuthApiRef,
gitlabAuthApiRef,
@@ -35,6 +36,9 @@ type ScopeMapping = {
repoWrite: string[];
};
// An enum of all supported providers
type ProviderName = 'generic' | 'github' | 'azure' | 'bitbucket' | 'gitlab';
class ScmAuthMux implements ScmAuthApi {
#providers: Array<ScmAuth>;
@@ -93,12 +97,14 @@ export class ScmAuth implements ScmAuthApi {
github: githubAuthApiRef,
gitlab: gitlabAuthApiRef,
azure: microsoftAuthApiRef,
bitbucket: bitbucketAuthApiRef,
},
factory: ({ github, gitlab, azure }) =>
factory: ({ github, gitlab, azure, bitbucket }) =>
ScmAuth.merge(
ScmAuth.forGithub(github),
ScmAuth.forGitlab(gitlab),
ScmAuth.forAzure(azure),
ScmAuth.forBitbucket(bitbucket),
),
});
}
@@ -116,7 +122,7 @@ export class ScmAuth implements ScmAuthApi {
};
},
): ScmAuth {
return new ScmAuth(authApi, options.host, options.scopeMapping);
return new ScmAuth('generic', authApi, options.host, options.scopeMapping);
}
/**
@@ -139,7 +145,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'github.com';
return new ScmAuth(githubAuthApi, host, {
return new ScmAuth('github', githubAuthApi, host, {
default: ['repo', 'read:org', 'read:user'],
repoWrite: ['gist'],
});
@@ -165,7 +171,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'gitlab.com';
return new ScmAuth(gitlabAuthApi, host, {
return new ScmAuth('gitlab', gitlabAuthApi, host, {
default: ['read_user', 'read_api', 'read_repository'],
repoWrite: ['write_repository', 'api'],
});
@@ -191,7 +197,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'dev.azure.com';
return new ScmAuth(microsoftAuthApi, host, {
return new ScmAuth('azure', microsoftAuthApi, host, {
default: [
'vso.build',
'vso.code',
@@ -223,7 +229,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'bitbucket.org';
return new ScmAuth(bitbucketAuthApi, host, {
return new ScmAuth('bitbucket', bitbucketAuthApi, host, {
default: ['account', 'team', 'pullrequest', 'snippet', 'issue'],
repoWrite: ['pullrequest:write', 'snippet:write', 'issue:write'],
});
@@ -240,11 +246,18 @@ export class ScmAuth implements ScmAuthApi {
#api: OAuthApi;
#host: string;
#scopeMapping: ScopeMapping;
#providerName: ProviderName;
private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) {
private constructor(
providerName: ProviderName,
api: OAuthApi,
host: string,
scopeMapping: ScopeMapping,
) {
this.#api = api;
this.#host = host;
this.#scopeMapping = scopeMapping;
this.#providerName = providerName;
}
/**
@@ -254,6 +267,16 @@ export class ScmAuth implements ScmAuthApi {
return url.host === this.#host;
}
private getAdditionalScopesForProvider(
additionalScopes: ScmAuthTokenOptions['additionalScope'],
): string[] {
if (!additionalScopes?.customScopes || this.#providerName === 'generic') {
return [];
}
return additionalScopes.customScopes?.[this.#providerName] ?? [];
}
/**
* Fetches credentials for the given resource.
*/
@@ -267,7 +290,17 @@ export class ScmAuth implements ScmAuthApi {
scopes.push(...this.#scopeMapping.repoWrite);
}
const token = await this.#api.getAccessToken(scopes, restOptions);
const additionalScopes =
this.getAdditionalScopesForProvider(additionalScope);
if (additionalScopes.length) {
scopes.push(...additionalScopes);
}
const uniqueScopes = [...new Set(scopes)];
const token = await this.#api.getAccessToken(uniqueScopes, restOptions);
return {
token,
headers: {
@@ -44,6 +44,16 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions {
* the ability to create things like issues and pull requests.
*/
repoWrite?: boolean;
/**
* Allow an arbitrary list of scopes provided from the user
* to request from the provider.
*/
customScopes?: {
github?: string[];
azure?: string[];
bitbucket?: string[];
gitlab?: string[];
};
};
}
+2 -2
View File
@@ -39,9 +39,9 @@
"lodash": "^4.17.21"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/cli": "^0.13.1-next.1",
"@backstage/config-loader": "^0.9.3",
"@backstage/test-utils": "^0.2.3",
"@backstage/test-utils": "^0.2.4-next.0",
"@types/jest": "^26.0.7",
"@types/luxon": "^2.0.4",
"msw": "^0.35.0"
@@ -1,4 +1,31 @@
# embedded-techdocs-app
# techdocs-cli-embedded-app
## 0.2.62
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.7
- @backstage/cli@0.13.1
- @backstage/integration-react@0.1.20
- @backstage/core-app-api@0.5.2
- @backstage/plugin-techdocs@0.13.2
- @backstage/app-defaults@0.1.6
- @backstage/test-utils@0.2.4
- @backstage/plugin-catalog@0.7.11
## 0.2.62-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.7-next.1
- @backstage/cli@0.13.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
- @backstage/test-utils@0.2.4-next.0
## 0.2.62-next.0

Some files were not shown because too many files have changed in this diff Show More