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:
mufaddal motiwala
2021-12-17 00:30:47 +05:30
262 changed files with 5052 additions and 1221 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-github-actions': patch
---
Show empty state only when workflow API call has completed
+7
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
added useOwnedEntities hook to get the list of entities of the logged-in user
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Fixed bug in OwnershipCard component where text wasn't correctly pluralized
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-rails': patch
---
Add new options to rails new (force and skipTests)
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-common': minor
---
Create catalog-common and add catalog permissions.
+25
View File
@@ -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.
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Added OwnedEntityPicker field which displays Owned Entities in options
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Add support for passing paging parameters to the getEntities call of the catalog client
+9
View File
@@ -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.
-14
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
catalog:
- plugins/catalog/**/*
- plugins/catalog-*/**/*
- packages/catalog-*/**/*
scaffolder:
- plugins/scaffolder/**/*
- plugins/scaffolder-*/**/*
search:
- plugins/search/**/*
- plugins/search-*/**/*
- packages/search-*/**/*
+2
View File
@@ -12,6 +12,7 @@ Atlassian
automations
autoscaling
Autoscaling
autoselect
Avro
backrub
Bigtable
@@ -53,6 +54,7 @@ configs
const
cookiecutter
cron
cronjobs
css
Datadog
dataflow
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+12
View File
@@ -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
+1 -1
View File
@@ -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
+122
View File
@@ -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}
/>
}
/>
}
/>
```
+1 -1
View File
@@ -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.
+12
View File
@@ -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
View File
@@ -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: {},
+18
View File
@@ -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
View File
@@ -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 />
+7
View File
@@ -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
+3 -3
View File
@@ -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;
}
+14
View File
@@ -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
+10 -10
View File
@@ -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"
+9 -1
View File
@@ -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', () => {
+21 -8
View File
@@ -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) {
+70
View File
@@ -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';
+6
View File
@@ -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
+1
View File
@@ -325,6 +325,7 @@ interface LocationEntityV1alpha1 extends Entity {
type?: string;
target?: string;
targets?: string[];
presence?: 'required' | 'optional';
};
}
export { LocationEntityV1alpha1 as LocationEntity };
+2 -2
View File
@@ -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"]
}
}
}
+12
View File
@@ -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: {}
+6
View File
@@ -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
+5 -3
View File
@@ -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})/`],
};
+7 -7
View File
@@ -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",
+1 -1
View File
@@ -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]),
+2 -2
View File
@@ -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,
},
+9
View File
@@ -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 -1
View File
@@ -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",
+10
View File
@@ -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
+38 -13
View File
@@ -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
+4 -4
View File
@@ -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() {}
}
@@ -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);
});
});
@@ -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);
};
}
}
@@ -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);
},
);
});
@@ -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);
}
@@ -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;
+10
View File
@@ -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
+24 -5
View File
@@ -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;
+4 -4
View File
@@ -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;
+14
View File
@@ -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
+67 -41
View File
@@ -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
+3 -3
View File
@@ -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