Merge branch 'master' of https://github.com/mufaddal7/backstage-1 into plugin/new-relic-dashboard
Signed-off-by: mufaddal motiwala <mufaddalmm.52@gmail.com>
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-github-actions': patch
|
||||
---
|
||||
|
||||
Show empty state only when workflow API call has completed
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/app-defaults': patch
|
||||
'@backstage/core-app-api': patch
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
added useOwnedEntities hook to get the list of entities of the logged-in user
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Fixed bug in OwnershipCard component where text wasn't correctly pluralized
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-rails': patch
|
||||
---
|
||||
|
||||
Add new options to rails new (force and skipTests)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-common': minor
|
||||
---
|
||||
|
||||
Create catalog-common and add catalog permissions.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Deprecated the `CatalogClientWrapper` class.
|
||||
|
||||
The default implementation of `catalogApiRef` that this plugin exposes, is now powered by the new `fetchApiRef`. The default implementation of _that_ API, in turn, has the ability to inject the user's Backstage token in requests in a similar manner to what the deprecated `CatalogClientWrapper` used to do. The latter has therefore been taken out of the default catalog API implementation.
|
||||
|
||||
If you use a custom `fetchApiRef` implementation that does NOT issue tokens, or use a custom `catalogApiRef` implementation which does NOT use the default `fetchApiRef`, you can still for some time wrap your catalog API in this class to get back the old behavior:
|
||||
|
||||
```ts
|
||||
// Add this to your packages/app/src/plugins.ts if you want to get back the old
|
||||
// catalog client behavior:
|
||||
createApiFactory({
|
||||
api: catalogApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new CatalogClientWrapper({
|
||||
client: new CatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
}),
|
||||
}),
|
||||
```
|
||||
|
||||
But do consider migrating to making use of the `fetchApiRef` as soon as convenient, since the wrapper class will be removed in a future release.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
---
|
||||
|
||||
Add the ability to supply a custom `fetchApi`. In the default frontend app setup, this will use the `fetchApiRef` implementation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Added OwnedEntityPicker field which displays Owned Entities in options
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
---
|
||||
|
||||
Add support for passing paging parameters to the getEntities call of the catalog client
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
In @backstage/plugin-scaffolder - When user will have one option available in hostUrl or owner - autoselect and select component should be readonly.
|
||||
|
||||
in @backstage/core-components - Select component has extended API with few more props: native : boolean, disabled: boolean. native - if set to true - Select component will use native browser select picker (not rendered by Material UI lib ).
|
||||
disabled - if set to true - action on component will not be possible.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-apache-airflow': minor
|
||||
---
|
||||
|
||||
Introduces a new plugin for the Apache Airflow workflow management platform.
|
||||
This implementation has been tested with the Apache Airflow v2 API,
|
||||
authenticating with basic authentication through the Backstage proxy plugin.
|
||||
|
||||
Supported functionality includes:
|
||||
|
||||
- Information card of version information of the Airflow instance
|
||||
- Information card of instance health for the meta-database and scheduler
|
||||
- Table of DAGs with meta information and status, along with a link to view
|
||||
details in the Airflow UI
|
||||
@@ -0,0 +1,11 @@
|
||||
catalog:
|
||||
- plugins/catalog/**/*
|
||||
- plugins/catalog-*/**/*
|
||||
- packages/catalog-*/**/*
|
||||
scaffolder:
|
||||
- plugins/scaffolder/**/*
|
||||
- plugins/scaffolder-*/**/*
|
||||
search:
|
||||
- plugins/search/**/*
|
||||
- plugins/search-*/**/*
|
||||
- packages/search-*/**/*
|
||||
@@ -12,6 +12,7 @@ Atlassian
|
||||
automations
|
||||
autoscaling
|
||||
Autoscaling
|
||||
autoselect
|
||||
Avro
|
||||
backrub
|
||||
Bigtable
|
||||
@@ -53,6 +54,7 @@ configs
|
||||
const
|
||||
cookiecutter
|
||||
cron
|
||||
cronjobs
|
||||
css
|
||||
Datadog
|
||||
dataflow
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
name: Create Changeset PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Create Release Pull Request
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Fossa
|
||||
run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v3
|
||||
with:
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
sync-labels: true
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
recent activity from the author. It will be closed if no further activity occurs.
|
||||
If you are the author and the PR has been closed, feel free to re-open the PR and continue the contribution!
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: 3
|
||||
days-before-pr-close: 5
|
||||
exempt-pr-labels: reviewer-approved,awaiting-review
|
||||
stale-pr-label: stale
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -61,3 +61,125 @@ breaking change to make `theme` optional. This means that if you currently
|
||||
construct the themes that you pass on to `createApp` using `AppTheme` as an
|
||||
intermediate type, you will need to work around this in some way, for example by
|
||||
passing the themes to `createApp` more directly.
|
||||
|
||||
### Generic Auth API Refs
|
||||
|
||||
`Released 2021-12-16 in @backstage/core-plugin-api v0.3.1`
|
||||
|
||||
There are four auth Utility API references in `@backstage/core-plugin-api` that
|
||||
were too generic to be useful. The APIs in question are `auth0AuthApiRef`,
|
||||
`oauth2ApiRef`, `oidcAuthApiRef`, and `samlAuthApiRef`. The issue with these
|
||||
APIs was that they had no actual contract of what the backing auth provider was.
|
||||
This made it more or less impossible to use these providers in open source
|
||||
plugins in any meaningful way. We also did not want to keep these Utility API
|
||||
references around just as helpers either, instead opting to remove them and let
|
||||
integrators define their own APIs that are more specific to their auth provider.
|
||||
This is also falls in line with a long-term goal to unify all auth providers to
|
||||
not have separate frontend implementations.
|
||||
|
||||
If you're currently using one of these API references for either Sign-In or
|
||||
access delegation within an app, there are a couple of steps you need to take to
|
||||
migrate to your own custom API.
|
||||
|
||||
First, you'll need to define a new Utility API reference. If you're only using
|
||||
the API for sign-in, you can put the definition in `packages/app/src/apis.ts`.
|
||||
However, if you need to access your auth API inside plugins you you'll need to
|
||||
export it from a common package. If you don't already have one we recommended
|
||||
creating `@internal/apis` and from there export the API reference.
|
||||
|
||||
```ts
|
||||
// `ProfileInfoApi & BackstageIdentityApi & SessionApi` are required for sign-in
|
||||
// Include `OAuthApi & OpenIdConnectApi` only if applicable
|
||||
export const acmeAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
ProfileInfoApi &
|
||||
BackstageIdentityApi &
|
||||
SessionApi
|
||||
> = createApiRef({
|
||||
id: 'internal.auth.acme',
|
||||
});
|
||||
```
|
||||
|
||||
Next you'll want to wire up the API inside `packages/app/src/apis.ts`, which
|
||||
varies depending on which API you're replacing. If you for example are replacing
|
||||
the `oauth2ApiRef`, the factory might look like this:
|
||||
|
||||
```ts
|
||||
// oauth2
|
||||
createApiFactory({
|
||||
api: acmeAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
|
||||
OAuth2.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Provider specific factory implementations, copy the code you need into the
|
||||
factory method depending on which apiRef you previously used.
|
||||
|
||||
```ts
|
||||
// samlAuthApiRef
|
||||
SamlAuth.create({
|
||||
discoveryApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
});
|
||||
|
||||
// oidcAuthApiRef
|
||||
OAuth2.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
provider: {
|
||||
id: 'oidc',
|
||||
title: 'Your Identity Provider',
|
||||
icon: () => null,
|
||||
},
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
});
|
||||
|
||||
// auth0AuthApiRef
|
||||
OAuth2.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
provider: {
|
||||
id: 'auth0',
|
||||
title: 'Auth0',
|
||||
icon: () => null,
|
||||
},
|
||||
defaultScopes: ['openid', 'email', 'profile'],
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
});
|
||||
```
|
||||
|
||||
Finally, for the provider to show up in your settings menu, you also need to
|
||||
update the settings route in `packages/app/src/App.tsx` to pass the
|
||||
`acmeAuthApiRef` to the `UserSettingsPage`. This replaces all existing provider
|
||||
items, so you might want to add back any of the default ones that you are using
|
||||
from the
|
||||
[DefaultProviderSettings](https://github.com/backstage/backstage/blob/a3ec122170e0205fd3f9c307b98b1c5e4f55bf5f/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx#L35).
|
||||
|
||||
```tsx
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<UserSettingsPage
|
||||
providerSettings={
|
||||
<ProviderSettingsItem
|
||||
title="ACME"
|
||||
description="Provides sign-in via ACME"
|
||||
apiRef={acmeAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -48,6 +48,6 @@ The Microsoft provider is a structure with three configuration keys:
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
To add the provider to the frontend, add the `microsoftAuthApi` reference and
|
||||
To add the provider to the frontend, add the `microsoftAuthApiRef` reference and
|
||||
`SignInPage` component as shown in
|
||||
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
|
||||
|
||||
@@ -264,6 +264,12 @@ following objects:
|
||||
- horizontalpodautoscalers
|
||||
- ingresses
|
||||
|
||||
The following RBAC permissions are required on the batch API group for the
|
||||
following objects:
|
||||
|
||||
- jobs
|
||||
- cronjobs
|
||||
|
||||
## Surfacing your Kubernetes components as part of an entity
|
||||
|
||||
There are two ways to surface your Kubernetes components as part of an entity.
|
||||
|
||||
@@ -90,6 +90,18 @@ within your instance. The configuration options are documented in the
|
||||
The underlying functionality is using official ElasticSearch client version 7.x,
|
||||
meaning that ElasticSearch version 7 is the only one confirmed to be supported.
|
||||
|
||||
Should you need to create your own bespoke search experiences that require more
|
||||
than just a query translator (such as faceted search or Relay pagination), you
|
||||
can access the configuration of the search engine in order to create new elastic
|
||||
search clients. The version of the client need not be the same as one used
|
||||
internally by the elastic search engine plugin. For example:
|
||||
|
||||
```typescript
|
||||
import { Client } from '@elastic/elastic-search';
|
||||
|
||||
const client = searchEngine.newClient(options => new Client(options));
|
||||
```
|
||||
|
||||
## Example configurations
|
||||
|
||||
### AWS
|
||||
|
||||
+5
-4
@@ -55,9 +55,9 @@
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@microsoft/api-documenter": "^7.13.68",
|
||||
"@microsoft/api-extractor": "^7.18.7",
|
||||
"@microsoft/api-extractor-model": "^7.13.5",
|
||||
"@microsoft/api-documenter": "^7.13.77",
|
||||
"@microsoft/api-extractor": "^7.19.2",
|
||||
"@microsoft/api-extractor-model": "^7.15.1",
|
||||
"@microsoft/tsdoc": "^0.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -78,11 +78,12 @@
|
||||
"prettier": "^2.2.1",
|
||||
"shx": "^0.3.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "~4.3.5",
|
||||
"yarn-lock-check": "^1.0.5"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"*.{js,jsx,ts,tsx,mjs,cjs}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
OneLoginAuth,
|
||||
UnhandledErrorForwarder,
|
||||
AtlassianAuth,
|
||||
createFetchApi,
|
||||
FetchMiddlewares,
|
||||
} from '@backstage/core-app-api';
|
||||
|
||||
import {
|
||||
@@ -42,6 +44,8 @@ import {
|
||||
analyticsApiRef,
|
||||
errorApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
oauthRequestApiRef,
|
||||
googleAuthApiRef,
|
||||
githubAuthApiRef,
|
||||
@@ -92,6 +96,27 @@ export const apis = [
|
||||
deps: { errorApi: errorApiRef },
|
||||
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: fetchApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
identityApi: identityApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
},
|
||||
factory: ({ configApi, identityApi, discoveryApi }) => {
|
||||
return createFetchApi({
|
||||
middleware: [
|
||||
FetchMiddlewares.resolvePluginProtocol({
|
||||
discoveryApi,
|
||||
}),
|
||||
FetchMiddlewares.injectIdentityAuth({
|
||||
identityApi,
|
||||
config: configApi,
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
}),
|
||||
createApiFactory({
|
||||
api: oauthRequestApiRef,
|
||||
deps: {},
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# example-app
|
||||
|
||||
## 0.2.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-github-actions@0.4.27
|
||||
- @backstage/core-app-api@0.2.1
|
||||
- @backstage/plugin-kubernetes@0.5.1
|
||||
- @backstage/core-plugin-api@0.3.1
|
||||
- @backstage/core-components@0.8.1
|
||||
- @backstage/plugin-org@0.3.31
|
||||
- @backstage/plugin-azure-devops@0.1.7
|
||||
- @backstage/cli@0.10.2
|
||||
- @backstage/catalog-model@0.9.8
|
||||
- @backstage/plugin-techdocs@0.12.10
|
||||
- @backstage/plugin-catalog-react@0.6.7
|
||||
- @backstage/plugin-apache-airflow@0.1.0
|
||||
|
||||
## 0.2.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+13
-13
@@ -1,40 +1,40 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.2.56",
|
||||
"version": "0.2.57",
|
||||
"private": true,
|
||||
"bundled": true,
|
||||
"dependencies": {
|
||||
"@backstage/app-defaults": "^0.1.2",
|
||||
"@backstage/catalog-model": "^0.9.7",
|
||||
"@backstage/cli": "^0.10.1",
|
||||
"@backstage/core-app-api": "^0.2.0",
|
||||
"@backstage/core-components": "^0.8.0",
|
||||
"@backstage/core-plugin-api": "^0.3.0",
|
||||
"@backstage/catalog-model": "^0.9.8",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@backstage/core-app-api": "^0.2.1",
|
||||
"@backstage/core-components": "^0.8.1",
|
||||
"@backstage/core-plugin-api": "^0.3.1",
|
||||
"@backstage/integration-react": "^0.1.15",
|
||||
"@backstage/plugin-api-docs": "^0.6.18",
|
||||
"@backstage/plugin-azure-devops": "^0.1.6",
|
||||
"@backstage/plugin-apache-airflow": "^0.0.0",
|
||||
"@backstage/plugin-azure-devops": "^0.1.7",
|
||||
"@backstage/plugin-apache-airflow": "^0.1.0",
|
||||
"@backstage/plugin-badges": "^0.2.16",
|
||||
"@backstage/plugin-catalog": "^0.7.4",
|
||||
"@backstage/plugin-catalog-graph": "^0.2.3",
|
||||
"@backstage/plugin-catalog-import": "^0.7.5",
|
||||
"@backstage/plugin-catalog-react": "^0.6.5",
|
||||
"@backstage/plugin-catalog-react": "^0.6.7",
|
||||
"@backstage/plugin-circleci": "^0.2.31",
|
||||
"@backstage/plugin-cloudbuild": "^0.2.29",
|
||||
"@backstage/plugin-code-coverage": "^0.1.19",
|
||||
"@backstage/plugin-cost-insights": "^0.11.13",
|
||||
"@backstage/plugin-explore": "^0.3.22",
|
||||
"@backstage/plugin-gcp-projects": "^0.3.10",
|
||||
"@backstage/plugin-github-actions": "^0.4.26",
|
||||
"@backstage/plugin-github-actions": "^0.4.27",
|
||||
"@backstage/plugin-graphiql": "^0.2.24",
|
||||
"@backstage/plugin-home": "^0.4.7",
|
||||
"@backstage/plugin-jenkins": "^0.5.14",
|
||||
"@backstage/plugin-kafka": "^0.2.22",
|
||||
"@backstage/plugin-kubernetes": "^0.5.0",
|
||||
"@backstage/plugin-kubernetes": "^0.5.1",
|
||||
"@backstage/plugin-lighthouse": "^0.2.31",
|
||||
"@backstage/plugin-newrelic": "^0.3.10",
|
||||
"@backstage/plugin-newrelic-dashboard": "^0.1.0",
|
||||
"@backstage/plugin-org": "^0.3.30",
|
||||
"@backstage/plugin-org": "^0.3.31",
|
||||
"@backstage/plugin-pagerduty": "0.3.19",
|
||||
"@backstage/plugin-rollbar": "^0.3.20",
|
||||
"@backstage/plugin-scaffolder": "^0.11.14",
|
||||
@@ -42,7 +42,7 @@
|
||||
"@backstage/plugin-sentry": "^0.3.30",
|
||||
"@backstage/plugin-shortcuts": "^0.1.15",
|
||||
"@backstage/plugin-tech-radar": "^0.4.13",
|
||||
"@backstage/plugin-techdocs": "^0.12.9",
|
||||
"@backstage/plugin-techdocs": "^0.12.10",
|
||||
"@backstage/plugin-todo": "^0.1.16",
|
||||
"@backstage/plugin-user-settings": "^0.3.13",
|
||||
"@backstage/search-common": "^0.2.0",
|
||||
|
||||
@@ -132,6 +132,8 @@ import {
|
||||
|
||||
import React, { ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
const customEntityFilterKind = ['Component', 'API', 'System'];
|
||||
|
||||
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
|
||||
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
|
||||
|
||||
@@ -552,7 +554,10 @@ const userPage = (
|
||||
<EntityUserProfileCard variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityOwnershipCard variant="gridItem" />
|
||||
<EntityOwnershipCard
|
||||
variant="gridItem"
|
||||
entityFilterKind={customEntityFilterKind}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityLayout.Route>
|
||||
@@ -568,7 +573,10 @@ const groupPage = (
|
||||
<EntityGroupProfileCard variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityOwnershipCard variant="gridItem" />
|
||||
<EntityOwnershipCard
|
||||
variant="gridItem"
|
||||
entityFilterKind={customEntityFilterKind}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<EntityMembersListCard />
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @backstage/backend-common
|
||||
|
||||
## 0.9.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fe24bc9a32: Each plugin now saves to a separate sqlite database file when `connection.filename` is provided in the sqlite config.
|
||||
Any existing sqlite database files will be ignored.
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.9.13",
|
||||
"version": "0.9.14",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.10.1",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@backstage/test-utils": "^0.1.24",
|
||||
"@types/archiver": "^5.1.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
@@ -94,7 +94,7 @@
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/stoppable": "^1.1.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tar": "^6.1.1",
|
||||
"@types/unzipper": "^0.10.3",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"aws-sdk-mock": "^5.2.1",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { omit } from 'lodash';
|
||||
import path from 'path';
|
||||
import {
|
||||
createDatabaseClient,
|
||||
ensureDatabaseExists,
|
||||
@@ -170,28 +171,6 @@ describe('DatabaseManager', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses top level sqlite database filename if plugin config is not present', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: 'some-file-path',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('pluginwithoutconfig').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
expect.stringContaining('some-file-path'),
|
||||
);
|
||||
});
|
||||
|
||||
it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
@@ -214,6 +193,110 @@ describe('DatabaseManager', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if top level sqlite filename is provided', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: 'some-file-path',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
testManager.forPlugin('pluginwithoutconfig').getClient(),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('creates plugin-specific sqlite files when plugin config is not present', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
directory: 'sqlite-files',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('pluginwithoutconfig').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
path.join('sqlite-files', 'pluginwithoutconfig.sqlite'),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses sqlite directory from top level config and filename from plugin config', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
directory: 'sqlite-files',
|
||||
},
|
||||
plugin: {
|
||||
test: {
|
||||
connection: {
|
||||
filename: 'other.sqlite',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('test').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
path.join('sqlite-files', 'other.sqlite'),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses sqlite directory and filename from plugin config', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
directory: 'sqlite-files',
|
||||
},
|
||||
plugin: {
|
||||
test: {
|
||||
connection: {
|
||||
directory: 'custom-sqlite-files',
|
||||
filename: 'other.sqlite',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('test').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
path.join('custom-sqlite-files', 'other.sqlite'),
|
||||
);
|
||||
});
|
||||
|
||||
it('connects to a plugin database using a specific database name', async () => {
|
||||
// testdbname.connection.database is set in config
|
||||
await manager.forPlugin('testdbname').getClient();
|
||||
@@ -282,7 +365,10 @@ describe('DatabaseManager', () => {
|
||||
expect(baseConfig.get().client).toEqual('sqlite3');
|
||||
|
||||
// sqlite3 uses 'filename' instead of 'database'
|
||||
expect(overrides).toHaveProperty('connection.filename');
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
'plugin_with_different_client',
|
||||
);
|
||||
});
|
||||
|
||||
it('provides database client specific base from plugin connection string when client set under plugin', async () => {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
normalizeConnection,
|
||||
} from './connection';
|
||||
import { PluginDatabaseManager } from './types';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Provides a config lookup path for a plugin's config block.
|
||||
@@ -114,10 +115,18 @@ export class DatabaseManager {
|
||||
const connection = this.getConnectionConfig(pluginId);
|
||||
|
||||
if (this.getClientType(pluginId).client === 'sqlite3') {
|
||||
// sqlite database name should fallback to ':memory:' as a special case
|
||||
return (
|
||||
(connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:'
|
||||
);
|
||||
const sqliteFilename: string | undefined = (
|
||||
connection as Knex.Sqlite3ConnectionConfig
|
||||
).filename;
|
||||
|
||||
if (sqliteFilename === ':memory:') {
|
||||
return sqliteFilename;
|
||||
}
|
||||
|
||||
const sqliteDirectory =
|
||||
(connection as { directory?: string }).directory ?? '.';
|
||||
|
||||
return path.join(sqliteDirectory, sqliteFilename ?? `${pluginId}.sqlite`);
|
||||
}
|
||||
|
||||
const databaseName = (connection as Knex.ConnectionConfig)?.database;
|
||||
@@ -205,6 +214,17 @@ export class DatabaseManager {
|
||||
this.config.get('connection'),
|
||||
this.config.getString('client'),
|
||||
);
|
||||
|
||||
if (
|
||||
client === 'sqlite3' &&
|
||||
'filename' in baseConnection &&
|
||||
baseConnection.filename !== ':memory:'
|
||||
) {
|
||||
throw new Error(
|
||||
'`connection.filename` is not supported for the base sqlite connection. Prefer `connection.directory` or provide a filename for the plugin connection instead.',
|
||||
);
|
||||
}
|
||||
|
||||
// Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The
|
||||
// `database` property from the base connection is omitted unless `pluginDivisionMode`
|
||||
// is set to `schema`. SQLite3's `filename` property is an exception as this is used as a
|
||||
|
||||
@@ -73,28 +73,6 @@ describe('sqlite3', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('builds a persistent connection per database', () => {
|
||||
expect(
|
||||
buildSqliteDatabaseConfig(
|
||||
createConfig({
|
||||
filename: path.join('path', 'to', 'foo'),
|
||||
}),
|
||||
{
|
||||
connection: {
|
||||
database: 'my-database',
|
||||
},
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: path.join('path', 'to', 'foo', 'my-database.sqlite'),
|
||||
database: 'my-database',
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces the connection with an override', () => {
|
||||
expect(
|
||||
buildSqliteDatabaseConfig(createConfig(':memory:'), {
|
||||
|
||||
@@ -86,19 +86,6 @@ export function buildSqliteDatabaseConfig(
|
||||
overrides,
|
||||
);
|
||||
|
||||
// If we don't create an in-memory database, interpret the connection string
|
||||
// as a directory that contains multiple sqlite files based on the database
|
||||
// name.
|
||||
const database = (config.connection as Knex.ConnectionConfig).database;
|
||||
const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig;
|
||||
|
||||
if (database && sqliteConnection.filename !== ':memory:') {
|
||||
sqliteConnection.filename = path.join(
|
||||
sqliteConnection.filename,
|
||||
`${database}.sqlite`,
|
||||
);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# example-backend
|
||||
|
||||
## 0.2.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-search-backend-module-elasticsearch@0.0.7
|
||||
- @backstage/plugin-catalog-backend@0.19.2
|
||||
- @backstage/plugin-scaffolder-backend@0.15.17
|
||||
- @backstage/backend-common@0.9.14
|
||||
- @backstage/plugin-azure-devops-backend@0.2.5
|
||||
- @backstage/plugin-auth-backend@0.5.1
|
||||
- @backstage/catalog-model@0.9.8
|
||||
- example-app@0.2.57
|
||||
|
||||
## 0.2.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.2.56",
|
||||
"version": "0.2.57",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -24,16 +24,16 @@
|
||||
"migrate:create": "knex migrate:make -x ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.9.13",
|
||||
"@backstage/backend-common": "^0.9.14",
|
||||
"@backstage/catalog-client": "^0.5.2",
|
||||
"@backstage/catalog-model": "^0.9.7",
|
||||
"@backstage/catalog-model": "^0.9.8",
|
||||
"@backstage/config": "^0.1.10",
|
||||
"@backstage/integration": "^0.6.10",
|
||||
"@backstage/plugin-app-backend": "^0.3.19",
|
||||
"@backstage/plugin-auth-backend": "^0.5.0",
|
||||
"@backstage/plugin-azure-devops-backend": "^0.2.4",
|
||||
"@backstage/plugin-auth-backend": "^0.5.1",
|
||||
"@backstage/plugin-azure-devops-backend": "^0.2.5",
|
||||
"@backstage/plugin-badges-backend": "^0.1.13",
|
||||
"@backstage/plugin-catalog-backend": "^0.19.1",
|
||||
"@backstage/plugin-catalog-backend": "^0.19.2",
|
||||
"@backstage/plugin-code-coverage-backend": "^0.1.16",
|
||||
"@backstage/plugin-graphql-backend": "^0.1.9",
|
||||
"@backstage/plugin-jenkins-backend": "^0.1.9",
|
||||
@@ -41,11 +41,11 @@
|
||||
"@backstage/plugin-kafka-backend": "^0.2.12",
|
||||
"@backstage/plugin-proxy-backend": "^0.2.14",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.16",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.15.16",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.15.17",
|
||||
"@backstage/plugin-scaffolder-backend-module-rails": "^0.2.0",
|
||||
"@backstage/plugin-search-backend": "^0.2.8",
|
||||
"@backstage/plugin-search-backend-node": "^0.4.2",
|
||||
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.6",
|
||||
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.7",
|
||||
"@backstage/plugin-search-backend-module-pg": "^0.2.2",
|
||||
"@backstage/plugin-techdocs-backend": "^0.12.0",
|
||||
"@backstage/plugin-tech-insights-backend": "^0.1.3",
|
||||
@@ -56,7 +56,7 @@
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"azure-devops-node-api": "^11.0.1",
|
||||
"dockerode": "^3.3.1",
|
||||
"example-app": "^0.2.56",
|
||||
"example-app": "^0.2.57",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"express-prom-bundle": "^6.3.6",
|
||||
@@ -68,7 +68,7 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.10.1",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@types/dockerode": "^3.3.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5"
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface CatalogApi {
|
||||
|
||||
// @public
|
||||
export class CatalogClient implements CatalogApi {
|
||||
constructor(options: { discoveryApi: DiscoveryApi });
|
||||
constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi });
|
||||
addLocation(
|
||||
{ type, target, dryRun, presence }: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
@@ -121,6 +121,9 @@ export type CatalogEntitiesRequest = {
|
||||
| Record<string, string | symbol | (string | symbol)[]>
|
||||
| undefined;
|
||||
fields?: string[] | undefined;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
after?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -155,4 +158,9 @@ export type DiscoveryApi = {
|
||||
// @public
|
||||
export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE =
|
||||
'backstage.io/catalog-processing';
|
||||
|
||||
// @public
|
||||
export type FetchApi = {
|
||||
fetch: typeof fetch;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -175,6 +175,24 @@ describe('CatalogClient', () => {
|
||||
{ apiVersion: '2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds paging parameters properly', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
|
||||
expect(req.url.search).toBe('?offset=1&limit=2&after=%3D');
|
||||
return res(ctx.json([]));
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await client.getEntities(
|
||||
{ offset: 1, limit: 2, after: '=' },
|
||||
{ token },
|
||||
);
|
||||
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocationById', () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
stringifyLocationReference,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import fetch from 'cross-fetch';
|
||||
import crossFetch from 'cross-fetch';
|
||||
import {
|
||||
CATALOG_FILTER_EXISTS,
|
||||
AddLocationRequest,
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
CatalogEntityAncestorsResponse,
|
||||
} from './types/api';
|
||||
import { DiscoveryApi } from './types/discovery';
|
||||
import { FetchApi } from './types/fetch';
|
||||
|
||||
/**
|
||||
* A frontend and backend compatible client for communicating with the Backstage Catalog.
|
||||
@@ -46,9 +47,11 @@ import { DiscoveryApi } from './types/discovery';
|
||||
* */
|
||||
export class CatalogClient implements CatalogApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly fetchApi: FetchApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.fetchApi = options.fetchApi || { fetch: crossFetch };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +113,7 @@ export class CatalogClient implements CatalogApi {
|
||||
request?: CatalogEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogListResponse<Entity>> {
|
||||
const { filter = [], fields = [] } = request ?? {};
|
||||
const { filter = [], fields = [], offset, limit, after } = request ?? {};
|
||||
const filterItems = [filter].flat();
|
||||
const params: string[] = [];
|
||||
|
||||
@@ -141,6 +144,16 @@ export class CatalogClient implements CatalogApi {
|
||||
params.push(`fields=${fields.map(encodeURIComponent).join(',')}`);
|
||||
}
|
||||
|
||||
if (offset !== undefined) {
|
||||
params.push(`offset=${offset}`);
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
params.push(`limit=${limit}`);
|
||||
}
|
||||
if (after !== undefined) {
|
||||
params.push(`after=${encodeURIComponent(after)}`);
|
||||
}
|
||||
|
||||
const query = params.length ? `?${params.join('&')}` : '';
|
||||
const entities: Entity[] = await this.requestRequired(
|
||||
'GET',
|
||||
@@ -206,7 +219,7 @@ export class CatalogClient implements CatalogApi {
|
||||
* @public
|
||||
*/
|
||||
async refreshEntity(entityRef: string, options?: CatalogRequestOptions) {
|
||||
const response = await fetch(
|
||||
const response = await this.fetchApi.fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/refresh`,
|
||||
{
|
||||
headers: {
|
||||
@@ -237,7 +250,7 @@ export class CatalogClient implements CatalogApi {
|
||||
{ type = 'url', target, dryRun, presence }: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<AddLocationResponse> {
|
||||
const response = await fetch(
|
||||
const response = await this.fetchApi.fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
|
||||
dryRun ? '?dryRun=true' : ''
|
||||
}`,
|
||||
@@ -376,7 +389,7 @@ export class CatalogClient implements CatalogApi {
|
||||
const headers: Record<string, string> = options?.token
|
||||
? { Authorization: `Bearer ${options.token}` }
|
||||
: {};
|
||||
const response = await fetch(url, { method, headers });
|
||||
const response = await this.fetchApi.fetch(url, { method, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
@@ -392,7 +405,7 @@ export class CatalogClient implements CatalogApi {
|
||||
const headers: Record<string, string> = options?.token
|
||||
? { Authorization: `Bearer ${options.token}` }
|
||||
: {};
|
||||
const response = await fetch(url, { method, headers });
|
||||
const response = await this.fetchApi.fetch(url, { method, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
@@ -410,7 +423,7 @@ export class CatalogClient implements CatalogApi {
|
||||
const headers: Record<string, string> = options?.token
|
||||
? { Authorization: `Bearer ${options.token}` }
|
||||
: {};
|
||||
const response = await fetch(url, { method, headers });
|
||||
const response = await this.fetchApi.fetch(url, { method, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
|
||||
@@ -29,11 +29,81 @@ export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS');
|
||||
* @public
|
||||
*/
|
||||
export type CatalogEntitiesRequest = {
|
||||
/**
|
||||
* If given, return only entities that match the given patterns.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If multiple filter sets are given as an array, then there is effectively an
|
||||
* OR between each filter set.
|
||||
*
|
||||
* Within one filter set, there is effectively an AND between the various
|
||||
* keys.
|
||||
*
|
||||
* Within one key, if there are more than one value, then there is effectively
|
||||
* an OR between them.
|
||||
*
|
||||
* Example: For an input of
|
||||
*
|
||||
* ```
|
||||
* [
|
||||
* { kind: ['API', 'Component'] },
|
||||
* { 'metadata.name': 'a', 'metadata.namespace': 'b' }
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* This effectively means
|
||||
*
|
||||
* ```
|
||||
* (kind = EITHER 'API' OR 'Component')
|
||||
* OR
|
||||
* (metadata.name = 'a' AND metadata.namespace = 'b' )
|
||||
* ```
|
||||
*
|
||||
* Each key is a dot separated path in each object.
|
||||
*
|
||||
* As a value you can also pass in the symbol `CATALOG_FILTER_EXISTS`
|
||||
* (exported from this package), which means that you assert on the existence
|
||||
* of that key, no matter what its value is.
|
||||
*/
|
||||
filter?:
|
||||
| Record<string, string | symbol | (string | symbol)[]>[]
|
||||
| Record<string, string | symbol | (string | symbol)[]>
|
||||
| undefined;
|
||||
/**
|
||||
* If given, return only the parts of each entity that match those dot
|
||||
* separated paths in each object.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Example: For an input of `['kind', 'metadata.annotations']`, then response
|
||||
* objects will be shaped like
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* "kind": "Component",
|
||||
* "metadata": {
|
||||
* "annotations": {
|
||||
* "foo": "bar"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fields?: string[] | undefined;
|
||||
/**
|
||||
* If given, skips over the first N items in the result set.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* If given, returns at most N items from the result set.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* If given, skips over all items before that cursor as returned by a previous
|
||||
* request.
|
||||
*/
|
||||
after?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a copy of the core DiscoveryApi, to avoid importing core.
|
||||
* This is a copy of the DiscoveryApi, to avoid importing core-plugin-api.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a copy of FetchApi, to avoid importing core-plugin-api.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type FetchApi = {
|
||||
fetch: typeof fetch;
|
||||
};
|
||||
@@ -25,5 +25,6 @@ export type {
|
||||
CatalogEntityAncestorsResponse,
|
||||
} from './api';
|
||||
export type { DiscoveryApi } from './discovery';
|
||||
export type { FetchApi } from './fetch';
|
||||
export { CATALOG_FILTER_EXISTS } from './api';
|
||||
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @backstage/catalog-model
|
||||
|
||||
## 0.9.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ad7338bb48: Added an optional `presence` field to Location spec, which describes whether the target of a location is required to exist or not. It defaults to `'required'`, which is the current behaviour of the catalog.
|
||||
|
||||
## 0.9.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -325,6 +325,7 @@ interface LocationEntityV1alpha1 extends Entity {
|
||||
type?: string;
|
||||
target?: string;
|
||||
targets?: string[];
|
||||
presence?: 'required' | 'optional';
|
||||
};
|
||||
}
|
||||
export { LocationEntityV1alpha1 as LocationEntity };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/catalog-model",
|
||||
"description": "Types and validators that help describe the model of a Backstage Catalog",
|
||||
"version": "0.9.7",
|
||||
"version": "0.9.8",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.10.0",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"yaml": "^1.9.2"
|
||||
|
||||
@@ -102,4 +102,23 @@ describe('LocationV1alpha1Validator', () => {
|
||||
(entity as any).spec.targets = 7;
|
||||
await expect(validator.check(entity)).rejects.toThrow(/targets/);
|
||||
});
|
||||
|
||||
it('accepts good presence', async () => {
|
||||
(entity as any).spec.presence = 'required';
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
(entity as any).spec.presence = 'optional';
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('rejects empty presence', async () => {
|
||||
(entity as any).spec.presence = '';
|
||||
await expect(validator.check(entity)).rejects.toThrow(/presence/);
|
||||
});
|
||||
|
||||
it('rejects wrong presence', async () => {
|
||||
(entity as any).spec.presence = 7;
|
||||
await expect(validator.check(entity)).rejects.toThrow(/presence/);
|
||||
(entity as any).spec.presence = 'nope';
|
||||
await expect(validator.check(entity)).rejects.toThrow(/presence/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface LocationEntityV1alpha1 extends Entity {
|
||||
type?: string;
|
||||
target?: string;
|
||||
targets?: string[];
|
||||
presence?: 'required' | 'optional';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
],
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"type": "string",
|
||||
"description": "Whether the presence of the location target is required and it should be considered an error if it can not be found",
|
||||
"default": "required",
|
||||
"examples": ["required"],
|
||||
"enum": ["required", "optional"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,4 +67,16 @@ ignore:
|
||||
reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code
|
||||
expires: 2022-03-06T17:18:55.019Z
|
||||
created: 2021-09-06T17:18:55.027Z
|
||||
|
||||
'snyk:lic:npm:rollup-plugin-dts:LGPL-3.0':
|
||||
- '*':
|
||||
reason: Backstage itself does not redistribute this dependency in minified form
|
||||
expires: 2031-09-06T17:18:55.027Z
|
||||
created: 2021-09-06T17:18:55.027Z
|
||||
|
||||
'snyk:lic:npm:axe-core:MPL-2.0':
|
||||
- '*':
|
||||
reason: Backstage itself does not redistribute this dependency in minified form
|
||||
expires: 2031-09-06T17:18:55.027Z
|
||||
created: 2021-09-06T17:18:55.027Z
|
||||
patch: {}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @backstage/cli
|
||||
|
||||
## 0.10.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25dfc2d483: Add support for `.cjs` and `.mjs` extensions in local and dependency modules.
|
||||
|
||||
## 0.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -96,20 +96,22 @@ async function getProjectConfig(targetPath, displayName) {
|
||||
rootDir: path.resolve(targetPath, 'src'),
|
||||
coverageDirectory: path.resolve(targetPath, 'coverage'),
|
||||
coverageProvider: 'v8',
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
||||
},
|
||||
|
||||
transform: {
|
||||
'\\.(js|jsx|ts|tsx)$': require.resolve('./jestSucraseTransform.js'),
|
||||
'\\.(js|jsx|ts|tsx|mjs|cjs)$': require.resolve(
|
||||
'./jestSucraseTransform.js',
|
||||
),
|
||||
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$':
|
||||
require.resolve('./jestFileTransform.js'),
|
||||
'\\.(yaml)$': require.resolve('jest-transform-yaml'),
|
||||
},
|
||||
|
||||
// A bit more opinionated
|
||||
testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'],
|
||||
testMatch: ['**/?(*.)test.{js,jsx,ts,tsx,mjs,cjs}'],
|
||||
|
||||
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -116,11 +116,11 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.9.13",
|
||||
"@backstage/backend-common": "^0.9.14",
|
||||
"@backstage/config": "^0.1.11",
|
||||
"@backstage/core-components": "^0.8.0",
|
||||
"@backstage/core-plugin-api": "^0.3.0",
|
||||
"@backstage/core-app-api": "^0.2.0",
|
||||
"@backstage/core-components": "^0.8.1",
|
||||
"@backstage/core-plugin-api": "^0.3.1",
|
||||
"@backstage/core-app-api": "^0.2.1",
|
||||
"@backstage/dev-utils": "^0.2.14",
|
||||
"@backstage/test-utils": "^0.1.24",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
@@ -128,13 +128,13 @@
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/inquirer": "^7.3.1",
|
||||
"@types/inquirer": "^8.1.3",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
|
||||
"@types/rollup-plugin-postcss": "^2.0.0",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tar": "^6.1.1",
|
||||
"@types/terser-webpack-plugin": "^5.0.4",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-dev-server": "^3.11.5",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { paths } from '../lib/paths';
|
||||
|
||||
export default async (cmd: Command, cmdArgs: string[]) => {
|
||||
const args = [
|
||||
'--ext=js,jsx,ts,tsx',
|
||||
'--ext=js,jsx,ts,tsx,mjs,cjs',
|
||||
'--max-warnings=0',
|
||||
`--format=${cmd.format}`,
|
||||
...(cmdArgs ?? [paths.targetDir]),
|
||||
|
||||
@@ -61,7 +61,7 @@ export const transforms = (options: TransformOptions): Transforms => {
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(jsx?|mjs)$/,
|
||||
test: /\.(jsx?|mjs|cjs)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('@sucrase/webpack-loader'),
|
||||
options: {
|
||||
@@ -71,7 +71,7 @@ export const transforms = (options: TransformOptions): Transforms => {
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.m?js/,
|
||||
test: /\.(js|mjs|cjs)/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/codemods
|
||||
|
||||
## 0.1.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/core-app-api@0.2.1
|
||||
- @backstage/core-plugin-api@0.3.1
|
||||
- @backstage/core-components@0.8.1
|
||||
|
||||
## 0.1.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/codemods",
|
||||
"description": "A collection of codemods for Backstage projects",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @backstage/core-app-api
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c11ce4f552: Deprecated `Auth0Auth`, pointing to using `OAuth2` directly instead.
|
||||
- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`.
|
||||
- Updated dependencies
|
||||
- @backstage/core-plugin-api@0.3.1
|
||||
- @backstage/core-components@0.8.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -17,15 +17,14 @@ import { AppTheme } from '@backstage/core-plugin-api';
|
||||
import { AppThemeApi } from '@backstage/core-plugin-api';
|
||||
import { atlassianAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { auth0AuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { AuthProvider } from '@backstage/core-plugin-api';
|
||||
import { AuthRequester } from '@backstage/core-plugin-api';
|
||||
import { AuthRequesterOptions } from '@backstage/core-plugin-api';
|
||||
import { AuthProviderInfo } from '@backstage/core-plugin-api';
|
||||
import { AuthRequestOptions } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentity } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createApp as createApp_2 } from '@backstage/app-defaults';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
@@ -36,6 +35,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
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 { gitlabAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { googleAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
@@ -43,11 +43,13 @@ import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { OAuthApi } from '@backstage/core-plugin-api';
|
||||
import { OAuthRequestApi } from '@backstage/core-plugin-api';
|
||||
import { OAuthRequester } from '@backstage/core-plugin-api';
|
||||
import { OAuthRequesterOptions } from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { oktaAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { oneloginAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
|
||||
import { PendingAuthRequest } from '@backstage/core-plugin-api';
|
||||
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
|
||||
import { PluginOutput } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfoApi } from '@backstage/core-plugin-api';
|
||||
@@ -254,7 +256,7 @@ export class AtlassianAuth {
|
||||
static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export class Auth0Auth {
|
||||
// (undocumented)
|
||||
static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T;
|
||||
@@ -264,9 +266,7 @@ export class Auth0Auth {
|
||||
export type AuthApiCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
environment?: string;
|
||||
provider?: AuthProvider & {
|
||||
id: string;
|
||||
};
|
||||
provider?: AuthProviderInfo;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -320,6 +320,12 @@ export function createApp(
|
||||
options?: Parameters<typeof createApp_2>[0],
|
||||
): BackstageApp & AppContext;
|
||||
|
||||
// @public
|
||||
export function createFetchApi(options: {
|
||||
baseImplementation?: typeof fetch | undefined;
|
||||
middleware?: FetchMiddleware | FetchMiddleware[] | undefined;
|
||||
}): FetchApi;
|
||||
|
||||
// @public
|
||||
export function createSpecializedApp(options: AppOptions): BackstageApp;
|
||||
|
||||
@@ -371,6 +377,27 @@ export type FeatureFlaggedProps = {
|
||||
}
|
||||
);
|
||||
|
||||
// @public
|
||||
export interface FetchMiddleware {
|
||||
apply(next: typeof fetch): typeof fetch;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class FetchMiddlewares {
|
||||
static injectIdentityAuth(options: {
|
||||
identityApi: IdentityApi;
|
||||
config?: Config;
|
||||
urlPrefixAllowlist?: string[];
|
||||
header?: {
|
||||
name: string;
|
||||
value: (backstageToken: string) => string;
|
||||
};
|
||||
}): FetchMiddleware;
|
||||
static resolvePluginProtocol(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
}): FetchMiddleware;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null;
|
||||
|
||||
@@ -515,9 +542,9 @@ export type OAuthApiCreateOptions = AuthApiCreateOptions & {
|
||||
// @public
|
||||
export class OAuthRequestManager implements OAuthRequestApi {
|
||||
// (undocumented)
|
||||
authRequest$(): Observable<PendingAuthRequest[]>;
|
||||
authRequest$(): Observable<PendingOAuthRequest[]>;
|
||||
// (undocumented)
|
||||
createAuthRequester<T>(options: AuthRequesterOptions<T>): AuthRequester<T>;
|
||||
createAuthRequester<T>(options: OAuthRequesterOptions<T>): OAuthRequester<T>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -539,9 +566,7 @@ export type OneLoginAuthCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
environment?: string;
|
||||
provider?: AuthProvider & {
|
||||
id: string;
|
||||
};
|
||||
provider?: AuthProviderInfo;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core-app-api",
|
||||
"description": "Core app API used by Backstage apps",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -30,9 +30,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/app-defaults": "^0.1.2",
|
||||
"@backstage/core-components": "^0.8.0",
|
||||
"@backstage/core-components": "^0.8.1",
|
||||
"@backstage/config": "^0.1.11",
|
||||
"@backstage/core-plugin-api": "^0.3.0",
|
||||
"@backstage/core-plugin-api": "^0.3.1",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@backstage/types": "^0.1.1",
|
||||
"@backstage/version-bridge": "^0.1.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.10.1",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@backstage/test-utils": "^0.1.24",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 { Config } from '@backstage/config';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
|
||||
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
|
||||
import { FetchMiddleware } from './types';
|
||||
|
||||
/**
|
||||
* A collection of common middlewares for the FetchApi.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class FetchMiddlewares {
|
||||
/**
|
||||
* Handles translation from `plugin://` URLs to concrete http(s) URLs based on
|
||||
* the discovery API.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If the request is for `plugin://catalog/entities?filter=x=y`, the discovery
|
||||
* API will be queried for `'catalog'`. If it returned
|
||||
* `https://backstage.example.net/api/catalog`, the resulting query would be
|
||||
* `https://backstage.example.net/api/catalog/entities?filter=x=y`.
|
||||
*
|
||||
* If the incoming URL protocol was not `plugin`, the request is just passed
|
||||
* through verbatim to the underlying implementation.
|
||||
*/
|
||||
static resolvePluginProtocol(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
}): FetchMiddleware {
|
||||
return new PluginProtocolResolverFetchMiddleware(options.discoveryApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a Backstage token header when the user is signed in.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Per default, an `Authorization: Bearer <token>` is generated. This can be
|
||||
* customized using the `header` option.
|
||||
*
|
||||
* The header injection only happens on allowlisted URLs. Per default, if the
|
||||
* `config` option is passed in, the `backend.baseUrl` is allowlisted, unless
|
||||
* the `urlPrefixAllowlist` option is passed in, in which case it takes
|
||||
* precedence. If you pass in neither config nor an allowlist, the middleware
|
||||
* will have no effect.
|
||||
*/
|
||||
static injectIdentityAuth(options: {
|
||||
identityApi: IdentityApi;
|
||||
config?: Config;
|
||||
urlPrefixAllowlist?: string[];
|
||||
header?: {
|
||||
name: string;
|
||||
value: (backstageToken: string) => string;
|
||||
};
|
||||
}): FetchMiddleware {
|
||||
return IdentityAuthInjectorFetchMiddleware.create(options);
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
|
||||
|
||||
describe('IdentityAuthInjectorFetchMiddleware', () => {
|
||||
it('creates using defaults', async () => {
|
||||
const middleware = IdentityAuthInjectorFetchMiddleware.create({
|
||||
identityApi: undefined as any,
|
||||
});
|
||||
expect(middleware.urlPrefixAllowlist).toEqual([]);
|
||||
expect(middleware.headerName).toEqual('authorization');
|
||||
expect(middleware.headerValue('t')).toEqual('Bearer t');
|
||||
});
|
||||
|
||||
it('creates using config', async () => {
|
||||
const middleware = IdentityAuthInjectorFetchMiddleware.create({
|
||||
identityApi: undefined as any,
|
||||
config: new ConfigReader({
|
||||
backend: { baseUrl: 'https://example.com/api' },
|
||||
}),
|
||||
header: { name: 'auth', value: t => `${t}!` },
|
||||
});
|
||||
expect(middleware.urlPrefixAllowlist).toEqual(['https://example.com/api']);
|
||||
expect(middleware.headerName).toEqual('auth');
|
||||
expect(middleware.headerValue('t')).toEqual('t!');
|
||||
});
|
||||
|
||||
it('creates using explicit allowlist', async () => {
|
||||
const middleware = IdentityAuthInjectorFetchMiddleware.create({
|
||||
identityApi: undefined as any,
|
||||
config: new ConfigReader({
|
||||
backend: { baseUrl: 'https://example.com/api' },
|
||||
}),
|
||||
urlPrefixAllowlist: ['https://a.com', 'http://b.com:8080/'],
|
||||
});
|
||||
expect(middleware.urlPrefixAllowlist).toEqual([
|
||||
'https://a.com',
|
||||
'http://b.com:8080',
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the header only when a token is available', async () => {
|
||||
const tokenFunction = jest.fn();
|
||||
const identityApi = {
|
||||
getCredentials: tokenFunction,
|
||||
} as unknown as IdentityApi;
|
||||
|
||||
const middleware = new IdentityAuthInjectorFetchMiddleware(
|
||||
identityApi,
|
||||
['https://example.com'],
|
||||
'Authorization',
|
||||
token => `Bearer ${token}`,
|
||||
);
|
||||
const inner = jest.fn();
|
||||
const outer = middleware.apply(inner);
|
||||
|
||||
// No token available
|
||||
tokenFunction.mockResolvedValueOnce({ token: undefined });
|
||||
await outer(new Request('https://example.com'));
|
||||
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]);
|
||||
|
||||
// Supply a token, header gets added
|
||||
tokenFunction.mockResolvedValueOnce({ token: 'token' });
|
||||
await outer(new Request('https://example.com'));
|
||||
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
|
||||
['authorization', 'Bearer token'],
|
||||
]);
|
||||
|
||||
// Token no longer available
|
||||
tokenFunction.mockResolvedValueOnce({ token: undefined });
|
||||
await outer(new Request('https://example.com'));
|
||||
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not overwrite an existing header with the same name', async () => {
|
||||
const identityApi = {
|
||||
getCredentials: () => ({ token: 'token' }),
|
||||
} as unknown as IdentityApi;
|
||||
|
||||
const middleware = new IdentityAuthInjectorFetchMiddleware(
|
||||
identityApi,
|
||||
['https://example.com'],
|
||||
'Authorization',
|
||||
token => `Bearer ${token}`,
|
||||
);
|
||||
const inner = jest.fn();
|
||||
const outer = middleware.apply(inner);
|
||||
|
||||
// No token available
|
||||
await outer(new Request('https://example.com'));
|
||||
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([
|
||||
['authorization', 'Bearer token'],
|
||||
]);
|
||||
|
||||
// Supply a token, header gets added
|
||||
await outer(
|
||||
new Request('https://example.com', {
|
||||
headers: { authorization: 'do-not-clobber' },
|
||||
}),
|
||||
);
|
||||
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
|
||||
['authorization', 'do-not-clobber'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not affect requests outside the allowlist', async () => {
|
||||
const identityApi = {
|
||||
getCredentials: () => ({ token: 'token' }),
|
||||
} as unknown as IdentityApi;
|
||||
|
||||
const middleware = new IdentityAuthInjectorFetchMiddleware(
|
||||
identityApi,
|
||||
['https://example.com:8080/root'],
|
||||
'Authorization',
|
||||
token => `Bearer ${token}`,
|
||||
);
|
||||
|
||||
const inner = jest.fn();
|
||||
const outer = middleware.apply(inner);
|
||||
|
||||
await outer(new Request('https://example.com:8080/root'));
|
||||
await outer(new Request('https://example.com:8080/root/sub'));
|
||||
await outer(new Request('https://example.com:8080/root2'));
|
||||
await outer(new Request('https://example.com/root'));
|
||||
await outer(new Request('http://example.com:8080/root'));
|
||||
await outer(new Request('https://example.com/root'));
|
||||
|
||||
const no: string[][] = [];
|
||||
const yes: string[][] = [['authorization', 'Bearer token']];
|
||||
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual(yes);
|
||||
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual(yes);
|
||||
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual(no);
|
||||
expect([...inner.mock.calls[3][0].headers.entries()]).toEqual(no);
|
||||
expect([...inner.mock.calls[4][0].headers.entries()]).toEqual(no);
|
||||
expect([...inner.mock.calls[5][0].headers.entries()]).toEqual(no);
|
||||
});
|
||||
});
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 { Config } from '@backstage/config';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { FetchMiddleware } from './types';
|
||||
|
||||
/**
|
||||
* A fetch middleware, which injects a Backstage token header when the user is
|
||||
* signed in.
|
||||
*/
|
||||
export class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware {
|
||||
static create(options: {
|
||||
identityApi: IdentityApi;
|
||||
config?: Config;
|
||||
urlPrefixAllowlist?: string[];
|
||||
header?: {
|
||||
name: string;
|
||||
value: (backstageToken: string) => string;
|
||||
};
|
||||
}): IdentityAuthInjectorFetchMiddleware {
|
||||
const allowlist: string[] = [];
|
||||
if (options.urlPrefixAllowlist) {
|
||||
allowlist.push(...options.urlPrefixAllowlist);
|
||||
} else if (options.config) {
|
||||
allowlist.push(options.config.getString('backend.baseUrl'));
|
||||
}
|
||||
|
||||
const headerName = options.header?.name || 'authorization';
|
||||
const headerValue = options.header?.value || (token => `Bearer ${token}`);
|
||||
|
||||
return new IdentityAuthInjectorFetchMiddleware(
|
||||
options.identityApi,
|
||||
allowlist.map(prefix => prefix.replace(/\/$/, '')),
|
||||
headerName,
|
||||
headerValue,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly identityApi: IdentityApi,
|
||||
public readonly urlPrefixAllowlist: string[],
|
||||
public readonly headerName: string,
|
||||
public readonly headerValue: (pluginId: string) => string,
|
||||
) {}
|
||||
|
||||
apply(next: typeof fetch): typeof fetch {
|
||||
return async (input, init) => {
|
||||
// Skip this middleware if the header already exists, or if the URL
|
||||
// doesn't match any of the allowlist items, or if there was no token
|
||||
const request = new Request(input, init);
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
if (
|
||||
request.headers.get(this.headerName) ||
|
||||
!this.urlPrefixAllowlist.some(
|
||||
prefix =>
|
||||
request.url === prefix || request.url.startsWith(`${prefix}/`),
|
||||
) ||
|
||||
typeof token !== 'string' ||
|
||||
!token
|
||||
) {
|
||||
return next(input, init);
|
||||
}
|
||||
|
||||
request.headers.set(this.headerName, this.headerValue(token));
|
||||
return next(request);
|
||||
};
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
|
||||
|
||||
describe('PluginProtocolResolverFetchMiddleware', () => {
|
||||
it.each([['https://passthrough.com/a']])(
|
||||
'passes through regular URLs, %p',
|
||||
async url => {
|
||||
const resolve = jest.fn();
|
||||
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
|
||||
const middleware = new PluginProtocolResolverFetchMiddleware(
|
||||
discoveryApi,
|
||||
);
|
||||
const inner = jest.fn();
|
||||
const outer = middleware.apply(inner);
|
||||
|
||||
await outer(url);
|
||||
expect(inner.mock.calls[0][0]).toBe(url);
|
||||
expect(resolve).not.toBeCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
'plugin://my-plugin/sub/path',
|
||||
'my-plugin',
|
||||
'https://real.com/base',
|
||||
'https://real.com/base/sub/path',
|
||||
],
|
||||
[
|
||||
'plugin://my-plugin/sub/path/',
|
||||
'my-plugin',
|
||||
'https://real.com/base/',
|
||||
'https://real.com/base/sub/path/',
|
||||
],
|
||||
['plugin://x', 'x', 'http://real.com:8080', 'http://real.com:8080'],
|
||||
[
|
||||
'plugin://x/a/b?c=d&e=f#g',
|
||||
'x',
|
||||
'https://real.com/base',
|
||||
'https://real.com/base/a/b?c=d&e=f#g',
|
||||
],
|
||||
[
|
||||
'plugin://x?c=d&e=f#g',
|
||||
'x',
|
||||
'https://real.com:8080/base',
|
||||
'https://real.com:8080/base?c=d&e=f#g',
|
||||
],
|
||||
[
|
||||
'plugin://username:password@x?c=d&e=f#g',
|
||||
'x',
|
||||
'https://real.com:8080/base',
|
||||
'https://username:password@real.com:8080/base?c=d&e=f#g',
|
||||
],
|
||||
[
|
||||
'plugin://x?c=d&e=f#g',
|
||||
'x',
|
||||
'https://username:password@real.com:8080/base',
|
||||
'https://username:password@real.com:8080/base?c=d&e=f#g',
|
||||
],
|
||||
])(
|
||||
'resolves backstage URLs, %p',
|
||||
async (original, host, resolved, result) => {
|
||||
const resolve = jest.fn();
|
||||
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
|
||||
const middleware = new PluginProtocolResolverFetchMiddleware(
|
||||
discoveryApi,
|
||||
);
|
||||
const inner = jest.fn();
|
||||
const outer = middleware.apply(inner);
|
||||
|
||||
resolve.mockResolvedValueOnce(resolved);
|
||||
await outer(original);
|
||||
expect(inner.mock.calls[0][0]).toBe(result);
|
||||
expect(resolve).toHaveBeenLastCalledWith(host);
|
||||
},
|
||||
);
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { FetchMiddleware } from './types';
|
||||
|
||||
function join(left: string, right: string): string {
|
||||
if (!right || right === '/') {
|
||||
return left;
|
||||
}
|
||||
|
||||
return `${left.replace(/\/$/, '')}/${right.replace(/^\//, '')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles translation from plugin://some-plugin-id/<path> to concrete http(s)
|
||||
* URLs.
|
||||
*/
|
||||
export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware {
|
||||
constructor(private readonly discoveryApi: DiscoveryApi) {}
|
||||
|
||||
apply(next: typeof fetch): typeof fetch {
|
||||
return async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
const prefix = 'plugin://';
|
||||
|
||||
if (!request.url.startsWith(prefix)) {
|
||||
return next(input, init);
|
||||
}
|
||||
|
||||
// Switch to a known protocol, since browser URL parsing misbehaves wildly
|
||||
// on foreign protocols
|
||||
const { hostname, pathname, search, hash, username, password } = new URL(
|
||||
`http://${request.url.substring(prefix.length)}`,
|
||||
);
|
||||
|
||||
let base = await this.discoveryApi.getBaseUrl(hostname);
|
||||
if (username || password) {
|
||||
const baseUrl = new URL(base);
|
||||
const authority = `${username}${password ? `:${password}` : ''}@`;
|
||||
base = `${baseUrl.protocol}//${authority}${baseUrl.host}${baseUrl.pathname}`;
|
||||
}
|
||||
|
||||
const target = `${join(base, pathname)}${search}${hash}`;
|
||||
return next(target, request);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { FetchMiddleware } from './types';
|
||||
|
||||
/**
|
||||
* Builds a fetch API, based on the builtin fetch wrapped by a set of optional
|
||||
* middleware implementations that add behaviors.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The middleware are applied in reverse order, i.e. the last one will be
|
||||
* "closest" to the base implementation. Passing in `[M1, M2, M3]` effectively
|
||||
* leads to `M1(M2(M3(baseImplementation)))`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createFetchApi(options: {
|
||||
baseImplementation?: typeof fetch | undefined;
|
||||
middleware?: FetchMiddleware | FetchMiddleware[] | undefined;
|
||||
}): FetchApi {
|
||||
let result = options.baseImplementation || global.fetch;
|
||||
|
||||
const middleware = [options.middleware ?? []].flat().reverse();
|
||||
for (const m of middleware) {
|
||||
result = m.apply(result);
|
||||
}
|
||||
|
||||
return {
|
||||
fetch: result,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { createFetchApi } from './createFetchApi';
|
||||
export { FetchMiddlewares } from './FetchMiddlewares';
|
||||
export type { FetchMiddleware } from './types';
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A middleware that modifies the behavior of an ongoing fetch.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FetchMiddleware {
|
||||
/**
|
||||
* Applies this middleware to an inner implementation.
|
||||
*
|
||||
* @param next - The next, inner, implementation, that this middleware shall
|
||||
* call out to as part of the request cycle.
|
||||
*/
|
||||
apply(next: typeof fetch): typeof fetch;
|
||||
}
|
||||
@@ -16,14 +16,14 @@
|
||||
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthRequesterOptions,
|
||||
OAuthRequesterOptions,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { OAuthRequestManager } from './OAuthRequestManager';
|
||||
|
||||
export default class MockOAuthApi implements OAuthRequestApi {
|
||||
private readonly real = new OAuthRequestManager();
|
||||
|
||||
createAuthRequester<T>(options: AuthRequesterOptions<T>) {
|
||||
createAuthRequester<T>(options: OAuthRequesterOptions<T>) {
|
||||
return this.real.createAuthRequester(options);
|
||||
}
|
||||
|
||||
|
||||
+15
-9
@@ -16,9 +16,9 @@
|
||||
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
PendingAuthRequest,
|
||||
AuthRequester,
|
||||
AuthRequesterOptions,
|
||||
PendingOAuthRequest,
|
||||
OAuthRequester,
|
||||
OAuthRequesterOptions,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
|
||||
@@ -34,11 +34,17 @@ import { BehaviorSubject } from '../../../lib/subjects';
|
||||
* @public
|
||||
*/
|
||||
export class OAuthRequestManager implements OAuthRequestApi {
|
||||
private readonly subject = new BehaviorSubject<PendingAuthRequest[]>([]);
|
||||
private currentRequests: PendingAuthRequest[] = [];
|
||||
private readonly subject = new BehaviorSubject<PendingOAuthRequest[]>([]);
|
||||
private currentRequests: PendingOAuthRequest[] = [];
|
||||
private handlerCount = 0;
|
||||
|
||||
createAuthRequester<T>(options: AuthRequesterOptions<T>): AuthRequester<T> {
|
||||
createAuthRequester<T>(options: OAuthRequesterOptions<T>): OAuthRequester<T> {
|
||||
if (!options.provider.id) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'DEPRECATION WARNING: Not passing a provider id to createAuthRequester is deprecated, it will be required in the future',
|
||||
);
|
||||
}
|
||||
const handler = new OAuthPendingRequests<T>();
|
||||
|
||||
const index = this.handlerCount;
|
||||
@@ -67,8 +73,8 @@ export class OAuthRequestManager implements OAuthRequestApi {
|
||||
// Converts the pending request and popup options into a popup request that we can forward to subscribers.
|
||||
private makeAuthRequest(
|
||||
request: PendingRequest<any>,
|
||||
options: AuthRequesterOptions<any>,
|
||||
): PendingAuthRequest | undefined {
|
||||
options: OAuthRequesterOptions<any>,
|
||||
): PendingOAuthRequest | undefined {
|
||||
const { scopes } = request;
|
||||
if (!scopes) {
|
||||
return undefined;
|
||||
@@ -88,7 +94,7 @@ export class OAuthRequestManager implements OAuthRequestApi {
|
||||
};
|
||||
}
|
||||
|
||||
authRequest$(): Observable<PendingAuthRequest[]> {
|
||||
authRequest$(): Observable<PendingOAuthRequest[]> {
|
||||
return this.subject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,23 @@ const DEFAULT_PROVIDER = {
|
||||
* Implements the OAuth flow to Auth0 products.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link OAuth2} instead
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* OAuth2.create({
|
||||
* discoveryApi,
|
||||
* oauthRequestApi,
|
||||
* provider: {
|
||||
* id: 'auth0',
|
||||
* title: 'Auth0',
|
||||
* icon: () => null,
|
||||
* },
|
||||
* defaultScopes: ['openid', 'email', 'profile'],
|
||||
* environment: configApi.getOptionalString('auth.environment'),
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export default class Auth0Auth {
|
||||
static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import {
|
||||
oneloginAuthApiRef,
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
AuthProviderInfo,
|
||||
DiscoveryApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { OAuth2 } from '../oauth2';
|
||||
@@ -30,7 +30,7 @@ export type OneLoginAuthCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
environment?: string;
|
||||
provider?: AuthProvider & { id: string };
|
||||
provider?: AuthProviderInfo;
|
||||
};
|
||||
|
||||
const DEFAULT_PROVIDER = {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderInfo,
|
||||
DiscoveryApi,
|
||||
OAuthRequestApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
@@ -36,5 +36,5 @@ export type OAuthApiCreateOptions = AuthApiCreateOptions & {
|
||||
export type AuthApiCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
environment?: string;
|
||||
provider?: AuthProvider & { id: string };
|
||||
provider?: AuthProviderInfo;
|
||||
};
|
||||
|
||||
@@ -27,5 +27,6 @@ export * from './ConfigApi';
|
||||
export * from './DiscoveryApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './FeatureFlagsApi';
|
||||
export * from './FetchApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './StorageApi';
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthRequester,
|
||||
OAuthRequester,
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
AuthProviderInfo,
|
||||
DiscoveryApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { showLoginPopup } from '../loginPopup';
|
||||
@@ -36,7 +36,7 @@ type Options<AuthSession> = {
|
||||
* Information about the auth provider to be shown to the user.
|
||||
* The ID Must match the backend auth plugin configuration, for example 'google'.
|
||||
*/
|
||||
provider: AuthProvider & { id: string };
|
||||
provider: AuthProviderInfo;
|
||||
/**
|
||||
* API used to instantiate an auth requester.
|
||||
*/
|
||||
@@ -65,9 +65,9 @@ export class DefaultAuthConnector<AuthSession>
|
||||
{
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly environment: string;
|
||||
private readonly provider: AuthProvider & { id: string };
|
||||
private readonly provider: AuthProviderInfo;
|
||||
private readonly joinScopesFunc: (scopes: Set<string>) => string;
|
||||
private readonly authRequester: AuthRequester<AuthSession>;
|
||||
private readonly authRequester: OAuthRequester<AuthSession>;
|
||||
private readonly sessionTransform: (response: any) => Promise<AuthSession>;
|
||||
|
||||
constructor(options: Options<AuthSession>) {
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AuthProvider, DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { AuthProviderInfo, DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { showLoginPopup } from '../loginPopup';
|
||||
|
||||
type Options = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
environment?: string;
|
||||
provider: AuthProvider & { id: string };
|
||||
provider: AuthProviderInfo;
|
||||
};
|
||||
export class DirectAuthConnector<DirectAuthResponse> {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly environment: string | undefined;
|
||||
private readonly provider: AuthProvider & { id: string };
|
||||
private readonly provider: AuthProviderInfo;
|
||||
|
||||
constructor(options: Options) {
|
||||
const { discoveryApi, environment, provider } = options;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @backstage/core-components
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2c17e5b073: Items in `<SidebarSubmenu>` are now only active when their full path is active (including search parameters).
|
||||
- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`.
|
||||
- 1680a1c5ac: Add Missing Override Components Type for SidebarSpace, SidebarSpacer, and SidebarDivider Components.
|
||||
- Updated dependencies
|
||||
- @backstage/core-plugin-api@0.3.1
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -814,13 +814,10 @@ export type ResponseErrorPanelClassKey = 'text' | 'divider';
|
||||
export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "SelectComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function Select(props: SelectProps): JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SelectClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type SelectClassKey =
|
||||
| 'formControl'
|
||||
@@ -830,11 +827,18 @@ export type SelectClassKey =
|
||||
| 'checkbox'
|
||||
| 'root';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SelectInputBaseClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type SelectedItems = string | string[] | number | number[];
|
||||
|
||||
// @public (undocumented)
|
||||
export type SelectInputBaseClassKey = 'root' | 'input';
|
||||
|
||||
// @public (undocumented)
|
||||
export type SelectItem = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -1152,6 +1156,11 @@ export const SidebarDivider: React_2.ComponentType<
|
||||
}
|
||||
>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarDividerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type SidebarDividerClassKey = 'root';
|
||||
|
||||
// @public
|
||||
export const SidebarExpandButton: () => JSX.Element | null;
|
||||
|
||||
@@ -1760,6 +1769,11 @@ export const SidebarSpace: React_2.ComponentType<
|
||||
}
|
||||
>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarSpaceClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type SidebarSpaceClassKey = 'root';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarSpacer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -2029,6 +2043,11 @@ export const SidebarSpacer: React_2.ComponentType<
|
||||
}
|
||||
>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarSpacerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type SidebarSpacerClassKey = 'root';
|
||||
|
||||
// @public
|
||||
export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core-components",
|
||||
"description": "Core components used by Backstage plugins and apps",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.1",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.11",
|
||||
"@backstage/core-plugin-api": "^0.3.0",
|
||||
"@backstage/core-plugin-api": "^0.3.1",
|
||||
"@backstage/errors": "^0.1.5",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@material-table/core": "^3.1.0",
|
||||
@@ -72,8 +72,8 @@
|
||||
"react-dom": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/core-app-api": "^0.2.0",
|
||||
"@backstage/cli": "^0.10.1",
|
||||
"@backstage/core-app-api": "^0.2.1",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@backstage/test-utils": "^0.1.24",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
|
||||
@@ -22,7 +22,7 @@ import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import React, { useState } from 'react';
|
||||
import { isError } from '@backstage/errors';
|
||||
import { PendingAuthRequest } from '@backstage/core-plugin-api';
|
||||
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
|
||||
|
||||
export type LoginRequestListItemClassKey = 'root';
|
||||
|
||||
@@ -36,7 +36,7 @@ const useItemStyles = makeStyles<Theme>(
|
||||
);
|
||||
|
||||
type RowProps = {
|
||||
request: PendingAuthRequest;
|
||||
request: PendingOAuthRequest;
|
||||
busy: boolean;
|
||||
setBusy: (busy: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -21,17 +21,18 @@ import FormControl from '@material-ui/core/FormControl';
|
||||
import InputBase from '@material-ui/core/InputBase';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
} from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ClosedDropdown from './static/ClosedDropdown';
|
||||
import OpenedDropdown from './static/OpenedDropdown';
|
||||
|
||||
/** @public */
|
||||
export type SelectInputBaseClassKey = 'root' | 'input';
|
||||
|
||||
const BootstrapInput = withStyles(
|
||||
@@ -60,6 +61,7 @@ const BootstrapInput = withStyles(
|
||||
{ name: 'BackstageSelectInputBase' },
|
||||
)(InputBase);
|
||||
|
||||
/** @public */
|
||||
export type SelectClassKey =
|
||||
| 'formControl'
|
||||
| 'label'
|
||||
@@ -93,6 +95,7 @@ const useStyles = makeStyles(
|
||||
margin: 2,
|
||||
},
|
||||
checkbox: {},
|
||||
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -101,23 +104,28 @@ const useStyles = makeStyles(
|
||||
{ name: 'BackstageSelect' },
|
||||
);
|
||||
|
||||
type Item = {
|
||||
/** @public */
|
||||
export type SelectItem = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
type Selection = string | string[] | number | number[];
|
||||
/** @public */
|
||||
export type SelectedItems = string | string[] | number | number[];
|
||||
|
||||
export type SelectProps = {
|
||||
multiple?: boolean;
|
||||
items: Item[];
|
||||
items: SelectItem[];
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
selected?: Selection;
|
||||
onChange: (arg: Selection) => void;
|
||||
selected?: SelectedItems;
|
||||
onChange: (arg: SelectedItems) => void;
|
||||
triggerReset?: boolean;
|
||||
native?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export function SelectComponent(props: SelectProps) {
|
||||
const {
|
||||
multiple,
|
||||
@@ -127,9 +135,11 @@ export function SelectComponent(props: SelectProps) {
|
||||
selected,
|
||||
onChange,
|
||||
triggerReset,
|
||||
native = false,
|
||||
disabled = false,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [value, setValue] = useState<Selection>(
|
||||
const [value, setValue] = useState<SelectedItems>(
|
||||
selected || (multiple ? [] : ''),
|
||||
);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
@@ -145,11 +155,15 @@ export function SelectComponent(props: SelectProps) {
|
||||
}, [selected]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setValue(event.target.value as Selection);
|
||||
onChange(event.target.value as Selection);
|
||||
setValue(event.target.value as SelectedItems);
|
||||
onChange(event.target.value as SelectedItems);
|
||||
};
|
||||
|
||||
const handleClick = (event: React.ChangeEvent<any>) => {
|
||||
if (disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
setOpen(previous => {
|
||||
if (multiple && !(event.target instanceof HTMLElement)) {
|
||||
return true;
|
||||
@@ -175,6 +189,8 @@ export function SelectComponent(props: SelectProps) {
|
||||
<FormControl className={classes.formControl}>
|
||||
<Select
|
||||
value={value}
|
||||
native={native}
|
||||
disabled={disabled}
|
||||
data-testid="select"
|
||||
displayEmpty
|
||||
multiple={multiple}
|
||||
@@ -223,19 +239,26 @@ export function SelectComponent(props: SelectProps) {
|
||||
{placeholder && !multiple && (
|
||||
<MenuItem value={[]}>{placeholder}</MenuItem>
|
||||
)}
|
||||
{items &&
|
||||
items.map(item => (
|
||||
<MenuItem key={item.value} value={item.value}>
|
||||
{multiple && (
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={(value as any[]).includes(item.value) || false}
|
||||
className={classes.checkbox}
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
{native
|
||||
? items &&
|
||||
items.map(item => (
|
||||
<option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))
|
||||
: items &&
|
||||
items.map(item => (
|
||||
<MenuItem key={item.value} value={item.value}>
|
||||
{multiple && (
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={(value as any[]).includes(item.value) || false}
|
||||
className={classes.checkbox}
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</ClickAwayListener>
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
*/
|
||||
|
||||
export { SelectComponent as Select } from './Select';
|
||||
export type { SelectClassKey, SelectInputBaseClassKey } from './Select';
|
||||
export type {
|
||||
SelectClassKey,
|
||||
SelectedItems,
|
||||
SelectInputBaseClassKey,
|
||||
SelectItem,
|
||||
} from './Select';
|
||||
export type { ClosedDropdownClassKey } from './static/ClosedDropdown';
|
||||
export type { OpenedDropdownClassKey } from './static/OpenedDropdown';
|
||||
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
SidebarItemWithSubmenuContext,
|
||||
} from './config';
|
||||
import { SidebarSubmenu } from './SidebarSubmenu';
|
||||
import { isLocationMatch } from './utils';
|
||||
import { Location } from 'history';
|
||||
|
||||
export type SidebarItemClassKey =
|
||||
| 'root'
|
||||
@@ -172,7 +174,7 @@ const useStyles = makeStyles<BackstageTheme>(
|
||||
|
||||
function isSidebarItemWithSubmenuActive(
|
||||
submenu: ReactNode,
|
||||
locationPathname: string,
|
||||
currentLocation: Location,
|
||||
) {
|
||||
// Item is active if any of submenu items have active paths
|
||||
const toPathnames: string[] = [];
|
||||
@@ -193,8 +195,8 @@ function isSidebarItemWithSubmenuActive(
|
||||
}
|
||||
});
|
||||
isActive = toPathnames.some(to => {
|
||||
const toPathname = resolvePath(to);
|
||||
return locationPathname === toPathname.pathname;
|
||||
const toLocation = resolvePath(to);
|
||||
return isLocationMatch(currentLocation, toLocation);
|
||||
});
|
||||
return isActive;
|
||||
}
|
||||
@@ -207,8 +209,8 @@ const SidebarItemWithSubmenu = ({
|
||||
}: PropsWithChildren<SidebarItemWithSubmenuProps>) => {
|
||||
const classes = useStyles();
|
||||
const [isHoveredOn, setIsHoveredOn] = useState(false);
|
||||
const { pathname: locationPathname } = useLocation();
|
||||
const isActive = isSidebarItemWithSubmenuActive(children, locationPathname);
|
||||
const currentLocation = useLocation();
|
||||
const isActive = isSidebarItemWithSubmenuActive(children, currentLocation);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHoveredOn(true);
|
||||
@@ -548,6 +550,8 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export type SidebarSpaceClassKey = 'root';
|
||||
|
||||
export const SidebarSpace = styled('div')(
|
||||
{
|
||||
flex: 1,
|
||||
@@ -555,6 +559,8 @@ export const SidebarSpace = styled('div')(
|
||||
{ name: 'BackstageSidebarSpace' },
|
||||
);
|
||||
|
||||
export type SidebarSpacerClassKey = 'root';
|
||||
|
||||
export const SidebarSpacer = styled('div')(
|
||||
{
|
||||
height: 8,
|
||||
@@ -562,6 +568,8 @@ export const SidebarSpacer = styled('div')(
|
||||
{ name: 'BackstageSidebarSpacer' },
|
||||
);
|
||||
|
||||
export type SidebarDividerClassKey = 'root';
|
||||
|
||||
export const SidebarDivider = styled('hr')(
|
||||
{
|
||||
height: 1,
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
|
||||
import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
|
||||
import LibraryBooksOutlinedIcon from '@material-ui/icons/LibraryBooksOutlined';
|
||||
import WebOutlinedIcon from '@material-ui/icons/WebOutlined';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
@@ -34,7 +32,7 @@ import {
|
||||
import { SidebarSubmenuItem } from './SidebarSubmenuItem';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import CloudQueueIcon from '@material-ui/icons/CloudQueue';
|
||||
import SettingsApplications from '@material-ui/icons/SettingsApplications';
|
||||
import AppsIcon from '@material-ui/icons/Apps';
|
||||
import AcUnitIcon from '@material-ui/icons/AcUnit';
|
||||
import { SidebarSubmenu } from './SidebarSubmenu';
|
||||
|
||||
@@ -76,17 +74,7 @@ export const SampleScalableSidebar = () => (
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem title="Tools" to="/1" icon={BuildRoundedIcon} />
|
||||
<SidebarSubmenuItem title="APIs" to="/2" icon={CloudQueueIcon} />
|
||||
<SidebarSubmenuItem
|
||||
title="Services"
|
||||
to="/3"
|
||||
icon={SettingsApplications}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Libraries"
|
||||
to="/4"
|
||||
icon={LibraryBooksOutlinedIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem title="Websites" to="/5" icon={WebOutlinedIcon} />
|
||||
<SidebarSubmenuItem title="Components" to="/3" icon={AppsIcon} />
|
||||
<SidebarSubmenuItem
|
||||
title="Misc"
|
||||
to="/6"
|
||||
|
||||
@@ -29,6 +29,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
|
||||
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
|
||||
import { SidebarItemWithSubmenuContext } from './config';
|
||||
import { isLocationMatch } from './utils';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
item: {
|
||||
@@ -116,14 +117,13 @@ export type SidebarSubmenuItemProps = {
|
||||
export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
|
||||
const { title, to, icon: Icon, dropdownItems } = props;
|
||||
const classes = useStyles();
|
||||
const { pathname: locationPathname } = useLocation();
|
||||
const { pathname: toPathname } = useResolvedPath(to);
|
||||
const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext);
|
||||
const closeSubmenu = () => {
|
||||
setIsHoveredOn(false);
|
||||
};
|
||||
|
||||
let isActive = locationPathname === toPathname;
|
||||
const toLocation = useResolvedPath(to);
|
||||
const currentLocation = useLocation();
|
||||
let isActive = isLocationMatch(currentLocation, toLocation);
|
||||
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
const handleClickDropdown = () => {
|
||||
@@ -132,7 +132,8 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
|
||||
if (dropdownItems !== undefined) {
|
||||
dropdownItems.some(item => {
|
||||
const resolvedPath = resolvePath(item.to);
|
||||
isActive = locationPathname === resolvedPath.pathname;
|
||||
isActive = isLocationMatch(currentLocation, resolvedPath);
|
||||
return isActive;
|
||||
});
|
||||
return (
|
||||
<div className={classes.itemContainer}>
|
||||
|
||||
@@ -33,7 +33,12 @@ export {
|
||||
SidebarSpacer,
|
||||
SidebarScrollWrapper,
|
||||
} from './Items';
|
||||
export type { SidebarItemClassKey } from './Items';
|
||||
export type {
|
||||
SidebarItemClassKey,
|
||||
SidebarSpaceClassKey,
|
||||
SidebarSpacerClassKey,
|
||||
SidebarDividerClassKey,
|
||||
} from './Items';
|
||||
export { IntroCard, SidebarIntro } from './Intro';
|
||||
export type { SidebarIntroClassKey } from './Intro';
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { Location, Path } from 'history';
|
||||
import { isLocationMatch } from './utils';
|
||||
|
||||
describe('isLocationMatching', () => {
|
||||
let currentLocation: Location;
|
||||
let toLocation: Path;
|
||||
|
||||
it('return false when pathname in target and current location differ', async () => {
|
||||
currentLocation = {
|
||||
pathname: '/catalog',
|
||||
search: '?kind=component',
|
||||
state: null,
|
||||
hash: '',
|
||||
key: '',
|
||||
};
|
||||
toLocation = {
|
||||
pathname: '/catalog-a',
|
||||
search: '?kind=component',
|
||||
hash: '',
|
||||
};
|
||||
expect(isLocationMatch(currentLocation, toLocation)).toBe(false);
|
||||
});
|
||||
|
||||
it('return true when exact match between current and target location parameters', async () => {
|
||||
currentLocation = {
|
||||
pathname: '/catalog',
|
||||
search: '?kind=component',
|
||||
state: null,
|
||||
hash: '',
|
||||
key: '',
|
||||
};
|
||||
toLocation = { pathname: '/catalog', search: '?kind=component', hash: '' };
|
||||
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
|
||||
});
|
||||
|
||||
it('return true when target query parameters are subset of current location query parameters', async () => {
|
||||
currentLocation = {
|
||||
pathname: '/catalog',
|
||||
search: '?x=foo&y=bar',
|
||||
state: null,
|
||||
hash: '',
|
||||
key: '',
|
||||
};
|
||||
toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' };
|
||||
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
|
||||
});
|
||||
|
||||
it('return false when no matching query parameters between target and current location', async () => {
|
||||
currentLocation = {
|
||||
pathname: '/catalog',
|
||||
search: '?y=bar',
|
||||
state: null,
|
||||
hash: '',
|
||||
key: '',
|
||||
};
|
||||
toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' };
|
||||
expect(isLocationMatch(currentLocation, toLocation)).toBe(false);
|
||||
});
|
||||
|
||||
it('return true when query parameters match in different order', async () => {
|
||||
currentLocation = {
|
||||
pathname: '/catalog',
|
||||
search: '?y=bar&x=foo',
|
||||
state: null,
|
||||
hash: '',
|
||||
key: '',
|
||||
};
|
||||
toLocation = { pathname: '/catalog', search: '?x=foo&y=bar', hash: '' };
|
||||
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
|
||||
});
|
||||
|
||||
it('return true when there is a matching query parameter alongside extra parameters', async () => {
|
||||
currentLocation = {
|
||||
pathname: '/catalog',
|
||||
search: '?y=bar&x=foo',
|
||||
state: null,
|
||||
hash: '',
|
||||
key: '',
|
||||
};
|
||||
toLocation = { pathname: '/catalog', search: '', hash: '' };
|
||||
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Location, Path } from 'history';
|
||||
import { isEqual, isMatch } from 'lodash';
|
||||
import qs from 'qs';
|
||||
|
||||
export function isLocationMatch(currentLocation: Location, toLocation: Path) {
|
||||
const toDecodedSearch = new URLSearchParams(toLocation.search).toString();
|
||||
const toQueryParameters = qs.parse(toDecodedSearch);
|
||||
|
||||
const currentDecodedSearch = new URLSearchParams(
|
||||
currentLocation.search,
|
||||
).toString();
|
||||
const currentQueryParameters = qs.parse(currentDecodedSearch);
|
||||
|
||||
const matching =
|
||||
isEqual(toLocation.pathname, currentLocation.pathname) &&
|
||||
isMatch(currentQueryParameters, toQueryParameters);
|
||||
|
||||
return matching;
|
||||
}
|
||||
@@ -83,6 +83,9 @@ import {
|
||||
ItemCardHeaderClassKey,
|
||||
PageClassKey,
|
||||
SidebarClassKey,
|
||||
SidebarSpaceClassKey,
|
||||
SidebarSpacerClassKey,
|
||||
SidebarDividerClassKey,
|
||||
SidebarIntroClassKey,
|
||||
SidebarItemClassKey,
|
||||
SidebarPageClassKey,
|
||||
@@ -157,6 +160,9 @@ type BackstageComponentsNameToClassKey = {
|
||||
BackstageItemCardHeader: ItemCardHeaderClassKey;
|
||||
BackstagePage: PageClassKey;
|
||||
BackstageSidebar: SidebarClassKey;
|
||||
BackstageSidebarSpace: SidebarSpaceClassKey;
|
||||
BackstageSidebarSpacer: SidebarSpacerClassKey;
|
||||
BackstageSidebarDivider: SidebarDividerClassKey;
|
||||
BackstageSidebarIntro: SidebarIntroClassKey;
|
||||
BackstageSidebarItem: SidebarItemClassKey;
|
||||
BackstageSidebarPage: SidebarPageClassKey;
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @backstage/core-plugin-api
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 18d4f500af: Deprecated the `AnyAnalyticsContext` type and mark the `AnalyticsApi` experimental.
|
||||
- 8a7372cfd5: Deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `oidcAuthApiRef`, `samlAuthApiRef`, and marked the rest of the auth `ApiRef`s as experimental. For more information on how to address the deprecations, see https://backstage.io/docs/api/deprecations#generic-auth-api-refs.
|
||||
- 760791a642: Renamed `AuthProvider` to `AuthProviderInfo` and add a required 'id' property to match the majority of usage. The `AuthProvider` type without the `id` property still exists but is deprecated, and all usage of it without an `id` is deprecated as well. For example, calling `createAuthRequest` without a `provider.id` is deprecated and it will be required in the future.
|
||||
|
||||
The following types have been renamed. The old names are still exported but deprecated, and are scheduled for removal in a future release.
|
||||
|
||||
- Renamed `AuthRequesterOptions` to `OAuthRequesterOptions`
|
||||
- Renamed `AuthRequester` to `OAuthRequester`
|
||||
- Renamed `PendingAuthRequest` to `PendingOAuthRequest`
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -35,25 +35,26 @@ export type AlertMessage = {
|
||||
severity?: 'success' | 'info' | 'warning' | 'error';
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export type AnalyticsApi = {
|
||||
captureEvent(event: AnalyticsEvent): void;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const analyticsApiRef: ApiRef<AnalyticsApi>;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const AnalyticsContext: (options: {
|
||||
attributes: Partial<AnalyticsContextValue>;
|
||||
children: ReactNode;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type AnalyticsContextValue = CommonAnalyticsContext &
|
||||
AnyAnalyticsContext;
|
||||
// @alpha
|
||||
export type AnalyticsContextValue = CommonAnalyticsContext & {
|
||||
[param in string]: string | boolean | number | undefined;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export type AnalyticsEvent = {
|
||||
action: string;
|
||||
subject: string;
|
||||
@@ -62,12 +63,12 @@ export type AnalyticsEvent = {
|
||||
context: AnalyticsContextValue;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export type AnalyticsEventAttributes = {
|
||||
[attribute in string]: string | boolean | number;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export type AnalyticsTracker = {
|
||||
captureEvent: (
|
||||
action: string,
|
||||
@@ -79,7 +80,7 @@ export type AnalyticsTracker = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type AnyAnalyticsContext = {
|
||||
[param in string]: string | boolean | number | undefined;
|
||||
};
|
||||
@@ -194,7 +195,7 @@ export type AppThemeApi = {
|
||||
// @public
|
||||
export const appThemeApiRef: ApiRef<AppThemeApi>;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const atlassianAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
>;
|
||||
@@ -206,27 +207,26 @@ export function attachComponentData<P>(
|
||||
data: unknown,
|
||||
): void;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const auth0AuthApiRef: ApiRef<
|
||||
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type AuthProvider = Omit<AuthProviderInfo, 'id'>;
|
||||
|
||||
// @public
|
||||
export type AuthProvider = {
|
||||
export type AuthProviderInfo = {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthRequester<AuthResponse> = (
|
||||
scopes: Set<string>,
|
||||
) => Promise<AuthResponse>;
|
||||
// @public @deprecated (undocumented)
|
||||
export type AuthRequester<T> = OAuthRequester<T>;
|
||||
|
||||
// @public
|
||||
export type AuthRequesterOptions<AuthResponse> = {
|
||||
provider: AuthProvider;
|
||||
onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
export type AuthRequesterOptions<T> = OAuthRequesterOptions<T>;
|
||||
|
||||
// @public
|
||||
export type AuthRequestOptions = {
|
||||
@@ -272,7 +272,7 @@ export type BackstageUserIdentity = {
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const bitbucketAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
>;
|
||||
@@ -283,7 +283,7 @@ export type BootErrorPageProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export type CommonAnalyticsContext = {
|
||||
pluginId: string;
|
||||
routeRef: string;
|
||||
@@ -506,23 +506,31 @@ export enum FeatureFlagState {
|
||||
None = 0,
|
||||
}
|
||||
|
||||
// @public
|
||||
export type FetchApi = {
|
||||
fetch: typeof fetch;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const fetchApiRef: ApiRef<FetchApi>;
|
||||
|
||||
// @public
|
||||
export function getComponentData<T>(
|
||||
node: ReactNode,
|
||||
type: string,
|
||||
): T | undefined;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const githubAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
>;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const gitlabAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
>;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const googleAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
@@ -570,7 +578,7 @@ export type MergeParams<
|
||||
P2 extends AnyParams,
|
||||
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const microsoftAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
@@ -579,7 +587,7 @@ export const microsoftAuthApiRef: ApiRef<
|
||||
SessionApi
|
||||
>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const oauth2ApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
@@ -598,15 +606,28 @@ export type OAuthApi = {
|
||||
|
||||
// @public
|
||||
export type OAuthRequestApi = {
|
||||
createAuthRequester<AuthResponse>(
|
||||
options: AuthRequesterOptions<AuthResponse>,
|
||||
): AuthRequester<AuthResponse>;
|
||||
authRequest$(): Observable_2<PendingAuthRequest[]>;
|
||||
createAuthRequester<OAuthResponse>(
|
||||
options: OAuthRequesterOptions<OAuthResponse>,
|
||||
): OAuthRequester<OAuthResponse>;
|
||||
authRequest$(): Observable_2<PendingOAuthRequest[]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const oauthRequestApiRef: ApiRef<OAuthRequestApi>;
|
||||
|
||||
// @public
|
||||
export type OAuthRequester<TAuthResponse> = (
|
||||
scopes: Set<string>,
|
||||
) => Promise<TAuthResponse>;
|
||||
|
||||
// @public
|
||||
export type OAuthRequesterOptions<TOAuthResponse> = {
|
||||
provider: Omit<AuthProviderInfo, 'id'> & {
|
||||
id?: string;
|
||||
};
|
||||
onAuthRequest(scopes: Set<string>): Promise<TOAuthResponse>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type OAuthScope = string | string[];
|
||||
|
||||
@@ -616,7 +637,7 @@ export type Observable<T> = Observable_2<T>;
|
||||
// @public @deprecated
|
||||
export type Observer<T> = Observer_2<T>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const oidcAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
@@ -625,7 +646,7 @@ export const oidcAuthApiRef: ApiRef<
|
||||
SessionApi
|
||||
>;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const oktaAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
@@ -637,7 +658,7 @@ export const oktaAuthApiRef: ApiRef<
|
||||
// @public
|
||||
export type OldIconComponent = ComponentType<SvgIconProps>;
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export const oneloginAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
OpenIdConnectApi &
|
||||
@@ -679,10 +700,15 @@ export type PathParams<S extends string> = {
|
||||
[name in ParamNames<S>]: string;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type PendingAuthRequest = PendingOAuthRequest;
|
||||
|
||||
// @public
|
||||
export type PendingAuthRequest = {
|
||||
provider: AuthProvider;
|
||||
reject: () => void;
|
||||
export type PendingOAuthRequest = {
|
||||
provider: Omit<AuthProviderInfo, 'id'> & {
|
||||
id?: string;
|
||||
};
|
||||
reject(): void;
|
||||
trigger(): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -738,7 +764,7 @@ export type RouteRef<Params extends AnyParams = any> = {
|
||||
title?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const samlAuthApiRef: ApiRef<
|
||||
ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
>;
|
||||
@@ -803,7 +829,7 @@ export type TypesToApiRefs<T> = {
|
||||
[key in keyof T]: ApiRef<T[key]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @alpha
|
||||
export function useAnalytics(): AnalyticsTracker;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core-plugin-api",
|
||||
"description": "Core API used by Backstage plugins",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -45,8 +45,8 @@
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.10.1",
|
||||
"@backstage/core-app-api": "^0.2.0",
|
||||
"@backstage/cli": "^0.10.2",
|
||||
"@backstage/core-app-api": "^0.2.1",
|
||||
"@backstage/test-utils": "^0.1.24",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
|
||||
@@ -60,7 +60,7 @@ export const useAnalyticsContext = (): AnalyticsContextValue => {
|
||||
* Analytics contexts are additive, meaning the context ultimately emitted with
|
||||
* an event is the combination of all contexts in the parent tree.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export const AnalyticsContext = (options: {
|
||||
attributes: Partial<AnalyticsContextValue>;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
/**
|
||||
* Common analytics context attributes.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type CommonAnalyticsContext = {
|
||||
/**
|
||||
@@ -40,6 +40,7 @@ export type CommonAnalyticsContext = {
|
||||
* Allows arbitrary scalar values as context attributes too.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Will be removed, use `AnalyticsContextValue` instead
|
||||
*/
|
||||
export type AnyAnalyticsContext = {
|
||||
[param in string]: string | boolean | number | undefined;
|
||||
@@ -48,7 +49,8 @@ export type AnyAnalyticsContext = {
|
||||
/**
|
||||
* Analytics context envelope.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type AnalyticsContextValue = CommonAnalyticsContext &
|
||||
AnyAnalyticsContext;
|
||||
export type AnalyticsContextValue = CommonAnalyticsContext & {
|
||||
[param in string]: string | boolean | number | undefined;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ function useAnalyticsApi(): AnalyticsApi {
|
||||
/**
|
||||
* Gets a pre-configured analytics tracker.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export function useAnalytics(): AnalyticsTracker {
|
||||
const trackerRef = useRef<Tracker | null>(null);
|
||||
|
||||
@@ -21,7 +21,7 @@ import { AnalyticsContextValue } from '../../analytics/types';
|
||||
* Represents an event worth tracking in an analytics system that could inform
|
||||
* how users of a Backstage instance are using its features.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type AnalyticsEvent = {
|
||||
/**
|
||||
@@ -79,7 +79,7 @@ export type AnalyticsEvent = {
|
||||
* A structure allowing other arbitrary metadata to be provided by analytics
|
||||
* event emitters.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type AnalyticsEventAttributes = {
|
||||
[attribute in string]: string | boolean | number;
|
||||
@@ -89,7 +89,7 @@ export type AnalyticsEventAttributes = {
|
||||
* Represents a tracker with methods that can be called to track events in a
|
||||
* configured analytics service.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type AnalyticsTracker = {
|
||||
captureEvent: (
|
||||
@@ -103,6 +103,8 @@ export type AnalyticsTracker = {
|
||||
};
|
||||
|
||||
/**
|
||||
* **EXPERIMENTAL**
|
||||
*
|
||||
* The Analytics API is used to track user behavior in a Backstage instance.
|
||||
*
|
||||
* @remarks
|
||||
@@ -111,7 +113,7 @@ export type AnalyticsTracker = {
|
||||
* useAnalytics() hook. This will return a pre-configured AnalyticsTracker
|
||||
* with relevant methods for instrumentation.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export type AnalyticsApi = {
|
||||
/**
|
||||
@@ -122,9 +124,11 @@ export type AnalyticsApi = {
|
||||
};
|
||||
|
||||
/**
|
||||
* **EXPERIMENTAL**
|
||||
*
|
||||
* The {@link ApiRef} of {@link AnalyticsApi}.
|
||||
*
|
||||
* @public
|
||||
* @alpha
|
||||
*/
|
||||
export const analyticsApiRef: ApiRef<AnalyticsApi> = createApiRef({
|
||||
id: 'core.analytics',
|
||||
|
||||
@@ -41,7 +41,15 @@ export type Error = ErrorApiError;
|
||||
* @public
|
||||
*/
|
||||
export type ErrorApiErrorContext = {
|
||||
// If set to true, this error should not be displayed to the user. Defaults to false.
|
||||
/**
|
||||
* If set to true, this error should not be displayed to the user.
|
||||
*
|
||||
* Hidden errors are typically not displayed in the UI, but the ErrorApi
|
||||
* implementation may still report them to error tracking services
|
||||
* or other utilities that care about all errors.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { ApiRef, createApiRef } from '../system';
|
||||
|
||||
/**
|
||||
* A wrapper for the fetch API, that has additional behaviors such as the
|
||||
* ability to automatically inject auth information where necessary.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type FetchApi = {
|
||||
fetch: typeof fetch;
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper for the fetch API, that has additional behaviors such as the
|
||||
* ability to automatically inject auth information where necessary.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const fetchApiRef: ApiRef<FetchApi> = createApiRef({
|
||||
id: 'core.fetch',
|
||||
});
|
||||
@@ -14,31 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconComponent } from '../../icons/types';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { ApiRef, createApiRef } from '../system';
|
||||
import { AuthProviderInfo } from './auth';
|
||||
|
||||
/**
|
||||
* Information about the auth provider that we're requesting a login towards.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This should be shown to the user so that they can be informed about what login is being requested
|
||||
* before a popup is shown.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use AuthProviderInfo instead
|
||||
*/
|
||||
export type AuthProvider = {
|
||||
/**
|
||||
* Title for the auth provider, for example "GitHub"
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Icon for the auth provider.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
};
|
||||
export type AuthProvider = Omit<AuthProviderInfo, 'id'>;
|
||||
|
||||
/**
|
||||
* Describes how to handle auth requests. Both how to show them to the user, and what to do when
|
||||
@@ -46,26 +30,34 @@ export type AuthProvider = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AuthRequesterOptions<AuthResponse> = {
|
||||
export type OAuthRequesterOptions<TOAuthResponse> = {
|
||||
/**
|
||||
* Information about the auth provider, which will be forwarded to auth requests.
|
||||
*
|
||||
* Not passing in an `id` is deprecated, and it will be required in the future.
|
||||
*/
|
||||
provider: AuthProvider;
|
||||
provider: Omit<AuthProviderInfo, 'id'> & { id?: string };
|
||||
|
||||
/**
|
||||
* Implementation of the auth flow, which will be called synchronously when
|
||||
* trigger() is called on an auth requests.
|
||||
*/
|
||||
onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
|
||||
onAuthRequest(scopes: Set<string>): Promise<TOAuthResponse>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use OAuthRequesterOptions instead
|
||||
*/
|
||||
export type AuthRequesterOptions<T> = OAuthRequesterOptions<T>;
|
||||
|
||||
/**
|
||||
* Function used to trigger new auth requests for a set of scopes.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The returned promise will resolve to the same value returned by the onAuthRequest in the
|
||||
* {@link AuthRequesterOptions}. Or rejected, if the request is rejected.
|
||||
* {@link OAuthRequesterOptions}. Or rejected, if the request is rejected.
|
||||
*
|
||||
* This function can be called multiple times before the promise resolves. All calls
|
||||
* will be merged into one request, and the scopes forwarded to the onAuthRequest will be the
|
||||
@@ -73,9 +65,15 @@ export type AuthRequesterOptions<AuthResponse> = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AuthRequester<AuthResponse> = (
|
||||
export type OAuthRequester<TAuthResponse> = (
|
||||
scopes: Set<string>,
|
||||
) => Promise<AuthResponse>;
|
||||
) => Promise<TAuthResponse>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use OAuthRequester instead
|
||||
*/
|
||||
export type AuthRequester<T> = OAuthRequester<T>;
|
||||
|
||||
/**
|
||||
* An pending auth request for a single auth provider. The request will remain in this pending
|
||||
@@ -88,16 +86,18 @@ export type AuthRequester<AuthResponse> = (
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PendingAuthRequest = {
|
||||
export type PendingOAuthRequest = {
|
||||
/**
|
||||
* Information about the auth provider, as given in the AuthRequesterOptions
|
||||
*
|
||||
* Not passing in an `id` is deprecated, and it will be required in the future.
|
||||
*/
|
||||
provider: AuthProvider;
|
||||
provider: Omit<AuthProviderInfo, 'id'> & { id?: string };
|
||||
|
||||
/**
|
||||
* Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
|
||||
*/
|
||||
reject: () => void;
|
||||
reject(): void;
|
||||
|
||||
/**
|
||||
* Trigger the auth request to continue the auth flow, by for example showing a popup.
|
||||
@@ -107,6 +107,12 @@ export type PendingAuthRequest = {
|
||||
trigger(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use PendingOAuthRequest instead
|
||||
*/
|
||||
export type PendingAuthRequest = PendingOAuthRequest;
|
||||
|
||||
/**
|
||||
* Provides helpers for implemented OAuth login flows within Backstage.
|
||||
*
|
||||
@@ -125,9 +131,9 @@ export type OAuthRequestApi = {
|
||||
*
|
||||
* See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info.
|
||||
*/
|
||||
createAuthRequester<AuthResponse>(
|
||||
options: AuthRequesterOptions<AuthResponse>,
|
||||
): AuthRequester<AuthResponse>;
|
||||
createAuthRequester<OAuthResponse>(
|
||||
options: OAuthRequesterOptions<OAuthResponse>,
|
||||
): OAuthRequester<OAuthResponse>;
|
||||
|
||||
/**
|
||||
* Observers pending auth requests. The returned observable will emit all
|
||||
@@ -140,7 +146,7 @@ export type OAuthRequestApi = {
|
||||
* If a auth is triggered, and the auth handler resolves successfully, then all currently pending
|
||||
* AuthRequester calls will resolve to the value returned by the onAuthRequest call.
|
||||
*/
|
||||
authRequest$(): Observable<PendingAuthRequest[]>;
|
||||
authRequest$(): Observable<PendingOAuthRequest[]>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ApiRef, createApiRef } from '../system';
|
||||
import { IconComponent } from '../../icons/types';
|
||||
import { Observable } from '@backstage/types';
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,33 @@ import { Observable } from '@backstage/types';
|
||||
* const googleAuthApiRef = createApiRef<OAuthApi & OpenIDConnectApi>({ ... })
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about the auth provider.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This information is used both to connect the correct auth provider in the backend, as
|
||||
* well as displaying the provider to the user.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AuthProviderInfo = {
|
||||
/**
|
||||
* The ID of the auth provider. This should match with ID of the provider in the `@backstage/auth-backend`.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Title for the auth provider, for example "GitHub"
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Icon for the auth provider.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
/**
|
||||
* An array of scopes, or a scope string formatted according to the
|
||||
* auth provider, which is typically a space separated list.
|
||||
@@ -282,14 +310,14 @@ export type SessionApi = {
|
||||
/**
|
||||
* Provides authentication towards Google APIs and identities.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes.
|
||||
*
|
||||
* Note that the ID token payload is only guaranteed to contain the user's numerical Google ID,
|
||||
* email and expiration information. Do not rely on any other fields, as they might not be present.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const googleAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
@@ -304,12 +332,12 @@ export const googleAuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards GitHub APIs.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/}
|
||||
* for a full list of supported scopes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const githubAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
@@ -320,12 +348,12 @@ export const githubAuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards Okta APIs.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/}
|
||||
* for a full list of supported scopes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const oktaAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
@@ -340,12 +368,12 @@ export const oktaAuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards GitLab APIs.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token}
|
||||
* for a full list of supported scopes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const gitlabAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
@@ -362,6 +390,7 @@ export const gitlabAuthApiRef: ApiRef<
|
||||
* for a full list of supported scopes.
|
||||
*
|
||||
* @public
|
||||
* @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs
|
||||
*/
|
||||
export const auth0AuthApiRef: ApiRef<
|
||||
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
@@ -372,13 +401,13 @@ export const auth0AuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards Microsoft APIs and identities.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* For more info and a full list of supported scopes, see:
|
||||
* - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent}
|
||||
* - {@link https://docs.microsoft.com/en-us/graph/permissions-reference}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const microsoftAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
@@ -394,6 +423,7 @@ export const microsoftAuthApiRef: ApiRef<
|
||||
* Provides authentication for custom identity providers.
|
||||
*
|
||||
* @public
|
||||
* @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs
|
||||
*/
|
||||
export const oauth2ApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
@@ -409,6 +439,7 @@ export const oauth2ApiRef: ApiRef<
|
||||
* Provides authentication for custom OpenID Connect identity providers.
|
||||
*
|
||||
* @public
|
||||
* @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs
|
||||
*/
|
||||
export const oidcAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
@@ -424,6 +455,7 @@ export const oidcAuthApiRef: ApiRef<
|
||||
* Provides authentication for SAML-based identity providers.
|
||||
*
|
||||
* @public
|
||||
* @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs
|
||||
*/
|
||||
export const samlAuthApiRef: ApiRef<
|
||||
ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
@@ -434,7 +466,7 @@ export const samlAuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards OneLogin APIs.
|
||||
*
|
||||
* @public
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
*/
|
||||
export const oneloginAuthApiRef: ApiRef<
|
||||
OAuthApi &
|
||||
@@ -449,12 +481,11 @@ export const oneloginAuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards Bitbucket APIs.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/}
|
||||
* for a full list of supported scopes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const bitbucketAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
@@ -465,12 +496,11 @@ export const bitbucketAuthApiRef: ApiRef<
|
||||
/**
|
||||
* Provides authentication towards Atlassian APIs.
|
||||
*
|
||||
* @alpha This API is **EXPERIMENTAL** and might change in the future.
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/}
|
||||
* for a full list of supported scopes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const atlassianAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
|
||||
@@ -29,6 +29,7 @@ export * from './ConfigApi';
|
||||
export * from './DiscoveryApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './FeatureFlagsApi';
|
||||
export * from './FetchApi';
|
||||
export * from './IdentityApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './StorageApi';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user