Merge branch 'master' into mobile-sidebar

Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
Philipp Hugenroth
2021-12-20 09:21:36 +01:00
201 changed files with 3600 additions and 1130 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-github-actions': patch
---
Show empty state only when workflow API call has completed
-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
+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"
+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}
/>
}
/>
}
/>
```
+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"
],
+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,39 +1,39 @@
{
"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-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",
@@ -41,7 +41,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",
@@ -126,6 +126,8 @@ import {
} from '@roadiehq/backstage-plugin-travis-ci';
import React, { ReactNode, useMemo, useState } from 'react';
const customEntityFilterKind = ['Component', 'API', 'System'];
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
@@ -523,7 +525,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>
@@ -539,7 +544,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"
+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
+9 -13
View File
@@ -17,9 +17,7 @@ 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';
@@ -43,11 +41,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 +254,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 +264,7 @@ export class Auth0Auth {
export type AuthApiCreateOptions = {
discoveryApi: DiscoveryApi;
environment?: string;
provider?: AuthProvider & {
id: string;
};
provider?: AuthProviderInfo;
};
// @public
@@ -515,9 +513,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 +537,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",
@@ -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;
};
@@ -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
+15
View File
@@ -1166,6 +1166,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;
@@ -1795,6 +1800,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)
@@ -2064,6 +2074,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;
};
@@ -24,6 +24,8 @@ import Typography from '@material-ui/core/Typography';
import { CreateCSSProperties } from '@material-ui/core/styles/withStyles';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import SearchIcon from '@material-ui/icons/Search';
import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import classnames from 'classnames';
import React, {
forwardRef,
@@ -43,10 +45,13 @@ import {
SidebarContext,
SidebarItemWithSubmenuContext,
} from './config';
import { SidebarSubmenu } from './SidebarSubmenu';
import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import { SidebarSubmenuItemProps, SidebarSubmenuProps } from '.';
import {
SidebarSubmenuItemProps,
SidebarSubmenuProps,
SidebarSubmenu,
} from '.';
import { isLocationMatch } from './utils';
import { Location } from 'history';
export type SidebarItemClassKey =
| 'root'
@@ -226,7 +231,7 @@ const useLocationMatch = (
useElementFilter(
submenu.props.children,
elements => {
let isLocationMatch = false;
let active = false;
elements
.getElements()
.forEach(
@@ -235,25 +240,97 @@ const useLocationMatch = (
}: {
props: Partial<SidebarSubmenuItemProps>;
}) => {
if (!isLocationMatch) {
if (!active) {
if (dropdownItems?.length) {
dropdownItems.forEach(
({ to: _to }) =>
(isLocationMatch =
isLocationMatch || locationPathname.includes(_to)),
(active = active || locationPathname.includes(_to)),
);
return;
}
if (to) {
isLocationMatch = locationPathname.includes(to);
active = locationPathname.includes(to);
}
}
},
);
return isLocationMatch;
return active;
},
[locationPathname],
);
/*
function isSidebarItemWithSubmenuActive(
submenu: ReactNode,
currentLocation: Location,
) {
// Item is active if any of submenu items have active paths
const toPathnames: string[] = [];
let isActive = false;
let submenuItems: ReactNode;
Children.forEach(submenu, element => {
if (!React.isValidElement(element)) return;
submenuItems = element.props.children;
});
Children.forEach(submenuItems, element => {
if (!React.isValidElement(element)) return;
if (element.props.dropdownItems) {
element.props.dropdownItems.map((item: { to: string }) =>
toPathnames.push(item.to),
);
} else if (element.props.to) {
toPathnames.push(element.props.to);
}
});
isActive = toPathnames.some(to => {
const toLocation = resolvePath(to);
return isLocationMatch(currentLocation, toLocation);
});
return isActive;
}
const SidebarItemWithSubmenu = ({
text,
hasNotifications = false,
icon: Icon,
children,
}: PropsWithChildren<SidebarItemWithSubmenuProps>) => {
const classes = useStyles();
const [isHoveredOn, setIsHoveredOn] = useState(false);
const currentLocation = useLocation();
const isActive = isSidebarItemWithSubmenuActive(children, currentLocation);
const handleMouseEnter = () => {
setIsHoveredOn(true);
};
const handleMouseLeave = () => {
setIsHoveredOn(false);
};
const { isOpen } = useContext(SidebarContext);
const itemIcon = (
<Badge
color="secondary"
variant="dot"
overlap="circular"
className={isOpen ? '' : classes.closedItemIcon}
invisible={!hasNotifications}
>
<Icon fontSize="small" />
</Badge>
);
const openContent = (
<>
<div data-testid="login-button" className={classes.iconContainer}>
{itemIcon}
</div>
{text && (
<Typography variant="subtitle2" className={classes.label}>
{text}
</Typography>
)}
<div className={classes.secondaryAction}>{}</div>
</>
);*/
type SidebarItemBaseProps = {
icon: IconComponent;
@@ -570,6 +647,8 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) {
);
}
export type SidebarSpaceClassKey = 'root';
export const SidebarSpace = styled('div')(
{
flex: 1,
@@ -577,6 +656,8 @@ export const SidebarSpace = styled('div')(
{ name: 'BackstageSidebarSpace' },
);
export type SidebarSpacerClassKey = 'root';
export const SidebarSpacer = styled('div')(
{
height: 8,
@@ -584,6 +665,8 @@ export const SidebarSpacer = styled('div')(
{ name: 'BackstageSidebarSpacer' },
);
export type SidebarDividerClassKey = 'root';
export const SidebarDivider = styled('hr')(
{
height: 1,
@@ -19,12 +19,10 @@ import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
import MenuIcon from '@material-ui/icons/Menu';
import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
import LibraryBooksOutlinedIcon from '@material-ui/icons/LibraryBooksOutlined';
import WebOutlinedIcon from '@material-ui/icons/WebOutlined';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import CloudQueueIcon from '@material-ui/icons/CloudQueue';
import SettingsApplications from '@material-ui/icons/SettingsApplications';
import AcUnitIcon from '@material-ui/icons/AcUnit';
import AppsIcon from '@material-ui/icons/Apps';
import React, { ComponentType } from 'react';
import {
Sidebar,
@@ -83,17 +81,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"
@@ -30,6 +30,7 @@ import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
import { SidebarItemWithSubmenuContext } from './config';
import { SidebarContext } from '../..';
import { isLocationMatch } from './utils';
const useStyles = makeStyles<BackstageTheme>(theme => ({
item: {
@@ -121,8 +122,6 @@ 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 { setOpen } = useContext(SidebarContext);
@@ -132,8 +131,9 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
setOpen(false);
}
};
let isActive = locationPathname === toPathname;
const toLocation = useResolvedPath(to);
const currentLocation = useLocation();
let isActive = isLocationMatch(currentLocation, toLocation);
const [showDropDown, setShowDropDown] = useState(false);
const handleClickDropdown = () => {
@@ -142,7 +142,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}>
@@ -36,7 +36,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;
}
@@ -81,6 +81,11 @@ import {
CardActionsTopRightClassKey,
ItemCardGridClassKey,
ItemCardHeaderClassKey,
PageClassKey,
SidebarClassKey,
SidebarSpaceClassKey,
SidebarSpacerClassKey,
SidebarDividerClassKey,
SidebarIntroClassKey,
CustomProviderClassKey,
SignInPageClassKey,
@@ -151,6 +156,11 @@ type BackstageComponentsNameToClassKey = {
BackstageInfoCardCardActionsTopRight: CardActionsTopRightClassKey;
BackstageItemCardGrid: ItemCardGridClassKey;
BackstageItemCardHeader: ItemCardHeaderClassKey;
BackstagePage: PageClassKey;
BackstageSidebar: SidebarClassKey;
BackstageSidebarSpace: SidebarSpaceClassKey;
BackstageSidebarSpacer: SidebarSpacerClassKey;
BackstageSidebarDivider: SidebarDividerClassKey;
BackstageSidebarIntro: SidebarIntroClassKey;
BackstageCustomProvider: CustomProviderClassKey;
BackstageSignInPage: SignInPageClassKey;
+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
+59 -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;
@@ -512,17 +512,17 @@ export function getComponentData<T>(
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 +570,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 +579,7 @@ export const microsoftAuthApiRef: ApiRef<
SessionApi
>;
// @public
// @public @deprecated
export const oauth2ApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
@@ -598,15 +598,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 +629,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 +638,7 @@ export const oidcAuthApiRef: ApiRef<
SessionApi
>;
// @public
// @alpha
export const oktaAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
@@ -637,7 +650,7 @@ export const oktaAuthApiRef: ApiRef<
// @public
export type OldIconComponent = ComponentType<SvgIconProps>;
// @public
// @alpha
export const oneloginAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
@@ -679,10 +692,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 +756,7 @@ export type RouteRef<Params extends AnyParams = any> = {
title?: string;
};
// @public
// @public @deprecated
export const samlAuthApiRef: ApiRef<
ProfileInfoApi & BackstageIdentityApi & SessionApi
>;
@@ -803,7 +821,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;
};
@@ -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
+14
View File
@@ -1,5 +1,19 @@
# @backstage/create-app
## 0.4.8
### Patch Changes
- 25dfc2d483: Updated the root `package.json` to include files with `.cjs` and `.mjs` extensions in the `"lint-staged"` configuration.
To make this change to an existing app, apply the following changes to the `package.json` file:
```diff
"lint-staged": {
- "*.{js,jsx,ts,tsx}": [
+ "*.{js,jsx,ts,tsx,mjs,cjs}": [
```
## 0.4.7
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
"version": "0.4.7",
"version": "0.4.8",
"private": false,
"publishConfig": {
"access": "public"
@@ -41,7 +41,7 @@
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",
"@types/inquirer": "^7.3.1",
"@types/inquirer": "^8.1.3",
"@types/node": "^14.14.32",
"@types/recursive-readdir": "^2.2.0",
"mock-fs": "^5.1.1",
@@ -38,7 +38,7 @@
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"*.{js,jsx,ts,tsx,mjs,cjs}": [
"eslint --fix",
"prettier --write"
],
+1 -1
View File
@@ -59,7 +59,7 @@ module.exports = ({ args }) => {
},
},
{
test: /\.(jsx?|mjs)$/,
test: /\.(jsx?|mjs|cjs)$/,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
+4 -4
View File
@@ -23,8 +23,8 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.9.7",
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/plugin-catalog-react": "^0.6.5",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
@@ -37,8 +37,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/dev-utils": "^0.2.14",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
+4 -4
View File
@@ -22,8 +22,8 @@
},
"dependencies": {
"@backstage/config": "^0.1.5",
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -35,8 +35,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/dev-utils": "^0.2.14",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
+22
View File
@@ -0,0 +1,22 @@
# @backstage/plugin-apache-airflow
## 0.1.0
### Minor Changes
- 9aea335911: 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
### Patch Changes
- Updated dependencies
- @backstage/core-plugin-api@0.3.1
- @backstage/core-components@0.8.1
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
"version": "0.0.0",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -33,8 +33,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/dev-utils": "^0.2.14",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
+4 -4
View File
@@ -32,8 +32,8 @@
"dependencies": {
"@asyncapi/react-component": "^1.0.0-next.25",
"@backstage/catalog-model": "^0.9.7",
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/plugin-catalog": "^0.7.4",
"@backstage/plugin-catalog-react": "^0.6.5",
"@backstage/theme": "^0.2.14",
@@ -54,8 +54,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/dev-utils": "^0.2.14",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-auth-backend
## 0.5.1
### Patch Changes
- 699c2e9ddc: export minimal typescript types for OIDC provider
- Updated dependencies
- @backstage/backend-common@0.9.14
- @backstage/catalog-model@0.9.8
## 0.5.0
### Minor Changes
+40 -3
View File
@@ -47,6 +47,16 @@ export type AtlassianProviderOptions = {
};
};
// @public
export type AuthHandler<AuthResult> = (
input: AuthResult,
) => Promise<AuthHandlerResult>;
// @public
export type AuthHandlerResult = {
profile: ProfileInfo;
};
// Warning: (ae-missing-release-tag) "AuthProviderFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -258,7 +268,6 @@ export const createOAuth2Provider: (
options?: OAuth2ProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-forgotten-export) The symbol "OidcProviderOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -534,6 +543,20 @@ export type OAuthState = {
origin?: string;
};
// @public
export type OidcAuthResult = {
tokenset: TokenSet;
userinfo: UserinfoResponse;
};
// @public
export type OidcProviderOptions = {
authHandler?: AuthHandler<OidcAuthResult>;
signIn?: {
resolver?: SignInResolver<OidcAuthResult>;
};
};
// Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -606,6 +629,22 @@ export type SamlProviderOptions = {
};
};
// @public
export type SignInInfo<AuthResult> = {
profile: ProfileInfo;
result: AuthResult;
};
// @public
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger_2;
},
) => Promise<BackstageSignInResult>;
// Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -637,8 +676,6 @@ export type WebMessageResponse =
// Warnings were encountered during analysis:
//
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/atlassian/provider.d.ts:37:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
// src/providers/atlassian/provider.d.ts:42:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
// src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
"version": "0.5.0",
"version": "0.5.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,9 +30,9 @@
"clean": "backstage-cli clean"
},
"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.11",
"@backstage/errors": "^0.1.5",
"@backstage/test-utils": "^0.1.24",
@@ -73,7 +73,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.1",
"@backstage/cli": "^0.10.2",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/express-session": "^1.17.2",
@@ -34,6 +34,10 @@ export type {
AuthProviderRouteHandlers,
AuthProviderFactoryOptions,
AuthProviderFactory,
AuthHandler,
AuthHandlerResult,
SignInResolver,
SignInInfo,
} from './types';
// These types are needed for a postMessage from the login pop-up
@@ -13,5 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { OidcAuthResult, OidcProviderOptions } from './provider';
export { createOidcProvider } from './provider';
@@ -56,7 +56,11 @@ type OidcImpl = {
client: Client;
};
type AuthResult = {
/**
* authentication result for the OIDC which includes the token set and user information (a profile response sent by OIDC server)
* @public
*/
export type OidcAuthResult = {
tokenset: TokenSet;
userinfo: UserinfoResponse;
};
@@ -66,8 +70,8 @@ export type Options = OAuthProviderOptions & {
scope?: string;
prompt?: string;
tokenSignedResponseAlg?: string;
signInResolver?: SignInResolver<AuthResult>;
authHandler: AuthHandler<AuthResult>;
signInResolver?: SignInResolver<OidcAuthResult>;
authHandler: AuthHandler<OidcAuthResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
@@ -78,8 +82,8 @@ export class OidcAuthProvider implements OAuthHandlers {
private readonly scope?: string;
private readonly prompt?: string;
private readonly signInResolver?: SignInResolver<AuthResult>;
private readonly authHandler: AuthHandler<AuthResult>;
private readonly signInResolver?: SignInResolver<OidcAuthResult>;
private readonly authHandler: AuthHandler<OidcAuthResult>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
@@ -113,7 +117,7 @@ export class OidcAuthProvider implements OAuthHandlers {
): Promise<{ response: OAuthResponse; refreshToken?: string }> {
const { strategy } = await this.implementation;
const strategyResponse = await executeFrameHandlerStrategy<
AuthResult,
OidcAuthResult,
PrivateInfo
>(req, strategy);
const {
@@ -158,7 +162,7 @@ export class OidcAuthProvider implements OAuthHandlers {
(
tokenset: TokenSet,
userinfo: UserinfoResponse,
done: PassportDoneCallback<AuthResult, PrivateInfo>,
done: PassportDoneCallback<OidcAuthResult, PrivateInfo>,
) => {
if (typeof done !== 'function') {
throw new Error(
@@ -180,7 +184,7 @@ export class OidcAuthProvider implements OAuthHandlers {
// Use this function to grab the user profile info from the token
// Then populate the profile with it
private async handleResult(result: AuthResult): Promise<OAuthResponse> {
private async handleResult(result: OidcAuthResult): Promise<OAuthResponse> {
const { profile } = await this.authHandler(result);
const response: OAuthResponse = {
providerInfo: {
@@ -210,27 +214,37 @@ export class OidcAuthProvider implements OAuthHandlers {
}
}
export const oAuth2DefaultSignInResolver: SignInResolver<AuthResult> = async (
info,
ctx,
) => {
const { profile } = info;
export const oAuth2DefaultSignInResolver: SignInResolver<OidcAuthResult> =
async (info, ctx) => {
const { profile } = info;
if (!profile.email) {
throw new Error('Profile contained no email');
}
const userId = profile.email.split('@')[0];
const token = await ctx.tokenIssuer.issueToken({
claims: { sub: userId, ent: [`user:default/${userId}`] },
});
return { id: userId, token };
};
if (!profile.email) {
throw new Error('Profile contained no email');
}
const userId = profile.email.split('@')[0];
const token = await ctx.tokenIssuer.issueToken({
claims: { sub: userId, ent: [`user:default/${userId}`] },
});
return { id: userId, token };
};
/**
* OIDC provider callback options. An auth handler and a sign in resolver
* can be passed while creating a OIDC provider.
*
* authHandler : called after sign in was successful, a new object must be returned which includes a profile
* signInResolver: called after sign in was successful, expects to return a new {@link BackstageSignInResult}
*
* Both options are optional. There is fallback for authHandler where the default handler expect an e-mail explicitly
* otherwise it throws an error
*
* @public
*/
export type OidcProviderOptions = {
authHandler?: AuthHandler<AuthResult>;
authHandler?: AuthHandler<OidcAuthResult>;
signIn?: {
resolver?: SignInResolver<AuthResult>;
resolver?: SignInResolver<OidcAuthResult>;
};
};
@@ -260,7 +274,7 @@ export const createOidcProvider = (
tokenIssuer,
});
const authHandler: AuthHandler<AuthResult> = options?.authHandler
const authHandler: AuthHandler<OidcAuthResult> = options?.authHandler
? options.authHandler
: async ({ userinfo }) => ({
profile: {
@@ -271,7 +285,7 @@ export const createOidcProvider = (
});
const signInResolverFn =
options?.signIn?.resolver ?? oAuth2DefaultSignInResolver;
const signInResolver: SignInResolver<AuthResult> = info =>
const signInResolver: SignInResolver<OidcAuthResult> = info =>
signInResolverFn(info, {
catalogIdentityClient,
tokenIssuer,
@@ -240,6 +240,10 @@ export type ProfileInfo = {
picture?: string;
};
/**
* type of sign in information context, includes the profile information and authentication result which contains auth. related information
* @public
*/
export type SignInInfo<AuthResult> = {
/**
* The simple profile passed down for use in the frontend.
@@ -252,6 +256,11 @@ export type SignInInfo<AuthResult> = {
result: AuthResult;
};
/**
* Sign in resolver type describes the function which handles the result of a successful authentication
* and it must return a valid {@link BackstageSignInResult}
* @public
*/
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
context: {
@@ -261,6 +270,10 @@ export type SignInResolver<AuthResult> = (
},
) => Promise<BackstageSignInResult>;
/**
* The return type of authentication handler which must contain a valid profile information
* @public
*/
export type AuthHandlerResult = { profile: ProfileInfo };
/**
@@ -270,6 +283,8 @@ export type AuthHandlerResult = { profile: ProfileInfo };
*
* Throwing an error in the function will cause the authentication to fail, making it
* possible to use this function as a way to limit access to a certain group of users.
*
* @public
*/
export type AuthHandler<AuthResult> = (
input: AuthResult,
+61
View File
@@ -1,5 +1,66 @@
# @backstage/plugin-azure-devops-backend
## 0.2.5
### Patch Changes
- daf32e2c9b: Created some initial filters that can be used to create pull request columns:
- All
- AssignedToUser
- AssignedToCurrentUser
- AssignedToTeam
- AssignedToTeams
- AssignedToCurrentUsersTeams
- CreatedByUser
- CreatedByCurrentUser
- CreatedByTeam
- CreatedByTeams
- CreatedByCurrentUsersTeams
Example custom column creation:
```tsx
const COLUMN_CONFIGS: PullRequestColumnConfig[] = [
{
title: 'Created by me',
filters: [{ type: FilterType.CreatedByCurrentUser }],
},
{
title: 'Created by Backstage Core',
filters: [
{
type: FilterType.CreatedByTeam,
teamName: 'Backstage Core',
},
],
},
{
title: 'Assigned to my teams',
filters: [{ type: FilterType.AssignedToCurrentUsersTeams }],
},
{
title: 'Other PRs',
filters: [{ type: FilterType.All }],
simplified: true,
},
];
<Route
path="/azure-pull-requests"
element={
<AzurePullRequestsPage
projectName="{PROJECT_NAME}"
defaultColumnConfigs={COLUMN_CONFIGS}
/>
}
/>;
```
- Updated dependencies
- @backstage/backend-common@0.9.14
- @backstage/plugin-azure-devops-common@0.1.3
## 0.2.4
### Patch Changes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops-backend",
"version": "0.2.4",
"version": "0.2.5",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.9.13",
"@backstage/backend-common": "^0.9.14",
"@backstage/config": "^0.1.11",
"@backstage/plugin-azure-devops-common": "^0.1.2",
"@backstage/plugin-azure-devops-common": "^0.1.3",
"@types/express": "^4.17.6",
"azure-devops-node-api": "^11.0.1",
"express": "^4.17.1",
@@ -31,7 +31,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.1",
"@backstage/cli": "^0.10.2",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2",
"msw": "^0.35.0"
@@ -215,6 +215,7 @@ function convertReviewer(
return {
id: identityRef.id,
displayName: identityRef.displayName,
uniqueName: identityRef.uniqueName,
imageUrl: getAvatarUrl(identityRef),
isRequired: identityRef.isRequired,
isContainer: identityRef.isContainer,
+57
View File
@@ -1,5 +1,62 @@
# @backstage/plugin-azure-devops-common
## 0.1.3
### Patch Changes
- daf32e2c9b: Created some initial filters that can be used to create pull request columns:
- All
- AssignedToUser
- AssignedToCurrentUser
- AssignedToTeam
- AssignedToTeams
- AssignedToCurrentUsersTeams
- CreatedByUser
- CreatedByCurrentUser
- CreatedByTeam
- CreatedByTeams
- CreatedByCurrentUsersTeams
Example custom column creation:
```tsx
const COLUMN_CONFIGS: PullRequestColumnConfig[] = [
{
title: 'Created by me',
filters: [{ type: FilterType.CreatedByCurrentUser }],
},
{
title: 'Created by Backstage Core',
filters: [
{
type: FilterType.CreatedByTeam,
teamName: 'Backstage Core',
},
],
},
{
title: 'Assigned to my teams',
filters: [{ type: FilterType.AssignedToCurrentUsersTeams }],
},
{
title: 'Other PRs',
filters: [{ type: FilterType.All }],
simplified: true,
},
];
<Route
path="/azure-pull-requests"
element={
<AzurePullRequestsPage
projectName="{PROJECT_NAME}"
defaultColumnConfigs={COLUMN_CONFIGS}
/>
}
/>;
```
## 0.1.2
### Patch Changes
@@ -61,6 +61,10 @@ export interface CreatedBy {
// (undocumented)
imageUrl?: string;
// (undocumented)
teamIds?: string[];
// (undocumented)
teamNames?: string[];
// (undocumented)
uniqueName?: string;
}
@@ -254,6 +258,8 @@ export interface Reviewer {
// (undocumented)
isRequired?: boolean;
// (undocumented)
uniqueName?: string;
// (undocumented)
voteStatus: PullRequestVoteStatus;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops-common",
"version": "0.1.2",
"version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.10.1"
"@backstage/cli": "^0.10.2"
},
"files": [
"dist"
+3
View File
@@ -145,6 +145,7 @@ export interface DashboardPullRequest {
export interface Reviewer {
id?: string;
displayName?: string;
uniqueName?: string;
imageUrl?: string;
isRequired?: boolean;
isContainer?: boolean;
@@ -164,6 +165,8 @@ export interface CreatedBy {
displayName?: string;
uniqueName?: string;
imageUrl?: string;
teamIds?: string[];
teamNames?: string[];
}
export interface Repository {
+64
View File
@@ -1,5 +1,69 @@
# @backstage/plugin-azure-devops
## 0.1.7
### Patch Changes
- daf32e2c9b: Created some initial filters that can be used to create pull request columns:
- All
- AssignedToUser
- AssignedToCurrentUser
- AssignedToTeam
- AssignedToTeams
- AssignedToCurrentUsersTeams
- CreatedByUser
- CreatedByCurrentUser
- CreatedByTeam
- CreatedByTeams
- CreatedByCurrentUsersTeams
Example custom column creation:
```tsx
const COLUMN_CONFIGS: PullRequestColumnConfig[] = [
{
title: 'Created by me',
filters: [{ type: FilterType.CreatedByCurrentUser }],
},
{
title: 'Created by Backstage Core',
filters: [
{
type: FilterType.CreatedByTeam,
teamName: 'Backstage Core',
},
],
},
{
title: 'Assigned to my teams',
filters: [{ type: FilterType.AssignedToCurrentUsersTeams }],
},
{
title: 'Other PRs',
filters: [{ type: FilterType.All }],
simplified: true,
},
];
<Route
path="/azure-pull-requests"
element={
<AzurePullRequestsPage
projectName="{PROJECT_NAME}"
defaultColumnConfigs={COLUMN_CONFIGS}
/>
}
/>;
```
- Updated dependencies
- @backstage/core-plugin-api@0.3.1
- @backstage/core-components@0.8.1
- @backstage/plugin-azure-devops-common@0.1.3
- @backstage/catalog-model@0.9.8
- @backstage/plugin-catalog-react@0.6.7
## 0.1.6
### Patch Changes
+163
View File
@@ -6,9 +6,55 @@
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { Entity } from '@backstage/catalog-model';
import { SvgIconProps } from '@material-ui/core';
// Warning: (ae-missing-release-tag) "AllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AllFilter = BaseFilter & {
type: FilterType.All;
};
// Warning: (ae-missing-release-tag) "AssignedToTeamFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AssignedToTeamFilter = BaseFilter & {
type: FilterType.AssignedToTeam;
teamId: string;
};
// Warning: (ae-missing-release-tag) "AssignedToTeamsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AssignedToTeamsFilter = BaseFilter &
(
| {
type: FilterType.AssignedToTeams;
teamIds: string[];
}
| {
type: FilterType.AssignedToCurrentUsersTeams;
teamIds?: string[];
}
);
// Warning: (ae-missing-release-tag) "AssignedToUserFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AssignedToUserFilter = BaseFilter &
(
| {
type: FilterType.AssignedToUser;
email: string;
}
| {
type: FilterType.AssignedToCurrentUser;
email?: string;
}
);
// Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -25,11 +71,71 @@ export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element;
export const AzurePullRequestsPage: ({
projectName,
pollingInterval,
defaultColumnConfigs,
}: {
projectName?: string | undefined;
pollingInterval?: number | undefined;
defaultColumnConfigs?: PullRequestColumnConfig[] | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "BaseFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type BaseFilter = {
type: FilterType;
};
// Warning: (ae-missing-release-tag) "CreatedByTeamFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CreatedByTeamFilter = BaseFilter &
({
type: FilterType.CreatedByTeam;
} & (
| {
teamId: string;
}
| {
teamName: string;
}
));
// Warning: (ae-missing-release-tag) "CreatedByTeamsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CreatedByTeamsFilter = BaseFilter &
(
| ({
type: FilterType.CreatedByTeams;
} & (
| {
teamIds: string[];
}
| {
teamNames: string[];
}
))
| {
type: FilterType.CreatedByCurrentUsersTeams;
teamIds?: string[];
}
);
// Warning: (ae-missing-release-tag) "CreatedByUserFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CreatedByUserFilter = BaseFilter &
(
| {
type: FilterType.CreatedByUser;
email: string;
}
| {
type: FilterType.CreatedByCurrentUser;
email?: string;
}
);
// Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -48,10 +154,67 @@ export const EntityAzurePullRequestsContent: ({
defaultLimit?: number | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type Filter =
| AssignedToUserFilter
| CreatedByUserFilter
| AssignedToTeamFilter
| CreatedByTeamFilter
| AssignedToTeamsFilter
| CreatedByTeamsFilter
| AllFilter;
// Warning: (ae-missing-release-tag) "FilterType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum FilterType {
// (undocumented)
All = 'All',
// (undocumented)
AssignedToCurrentUser = 'AssignedToCurrentUser',
// (undocumented)
AssignedToCurrentUsersTeams = 'AssignedToCurrentUsersTeams',
// (undocumented)
AssignedToTeam = 'AssignedToTeam',
// (undocumented)
AssignedToTeams = 'AssignedToTeams',
// (undocumented)
AssignedToUser = 'AssignedToUser',
// (undocumented)
CreatedByCurrentUser = 'CreatedByCurrentUser',
// (undocumented)
CreatedByCurrentUsersTeams = 'CreatedByCurrentUsersTeams',
// (undocumented)
CreatedByTeam = 'CreatedByTeam',
// (undocumented)
CreatedByTeams = 'CreatedByTeams',
// (undocumented)
CreatedByUser = 'CreatedByUser',
}
// Warning: (ae-missing-release-tag) "isAzureDevOpsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const isAzureDevOpsAvailable: (entity: Entity) => boolean;
// Warning: (ae-missing-release-tag) "PullRequestColumnConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface PullRequestColumnConfig {
// (undocumented)
filters: Filter[];
// (undocumented)
simplified?: boolean;
// (undocumented)
title: string;
}
// Warning: (ae-missing-release-tag) "PullRequestFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean;
// (No @packageDocumentation comment for this package)
```
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops",
"version": "0.1.6",
"version": "0.1.7",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -27,12 +27,12 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.9.7",
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/catalog-model": "^0.9.8",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/errors": "^0.1.4",
"@backstage/plugin-azure-devops-common": "^0.1.2",
"@backstage/plugin-catalog-react": "^0.6.5",
"@backstage/plugin-azure-devops-common": "^0.1.3",
"@backstage/plugin-catalog-react": "^0.6.7",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -46,8 +46,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/dev-utils": "^0.2.14",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
@@ -21,56 +21,17 @@ import {
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { PullRequestGroup, PullRequestGroupConfig } from './lib/types';
import React, { useEffect, useState } from 'react';
import { getCreatedByUserFilter, getPullRequestGroups } from './lib/utils';
import { useDashboardPullRequests, useUserEmail } from '../../hooks';
import { PullRequestColumnConfig, PullRequestGroup } from './lib/types';
import React, { useState } from 'react';
import { getPullRequestGroupConfigs, getPullRequestGroups } from './lib/utils';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { FilterType } from './lib/filters';
import { PullRequestGrid } from './lib/PullRequestGrid';
function usePullRequestGroupConfigs(
userEmail: string | undefined,
): PullRequestGroupConfig[] {
const [pullRequestGroupConfigs, setPullRequestGroupConfigs] = useState<
PullRequestGroupConfig[]
>([]);
useEffect(() => {
const prGroupConfigs: PullRequestGroupConfig[] = [
{ title: 'Created by me', filter: getCreatedByUserFilter(userEmail) },
{ title: 'Other PRs', filter: _ => true, simplified: false },
];
setPullRequestGroupConfigs(prGroupConfigs);
}, [userEmail]);
return pullRequestGroupConfigs;
}
function usePullRequestGroups(
pullRequests: DashboardPullRequest[] | undefined,
pullRequestGroupConfigs: PullRequestGroupConfig[],
): PullRequestGroup[] {
const [pullRequestGroups, setPullRequestGroups] = useState<
PullRequestGroup[]
>([]);
useEffect(() => {
if (pullRequests) {
const groups = getPullRequestGroups(
pullRequests,
pullRequestGroupConfigs,
);
setPullRequestGroups(groups);
}
}, [pullRequests, pullRequestGroupConfigs]);
return pullRequestGroups;
}
import { useDashboardPullRequests } from '../../hooks';
import { useFilterProcessor } from './lib/hooks';
type PullRequestsPageContentProps = {
pullRequestGroups: PullRequestGroup[];
pullRequestGroups: PullRequestGroup[] | undefined;
loading: boolean;
error?: Error;
};
@@ -80,7 +41,7 @@ const PullRequestsPageContent = ({
loading,
error,
}: PullRequestsPageContentProps) => {
if (loading && pullRequestGroups.length <= 0) {
if (loading && (!pullRequestGroups || pullRequestGroups.length <= 0)) {
return <Progress />;
}
@@ -88,25 +49,50 @@ const PullRequestsPageContent = ({
return <ResponseErrorPanel error={error} />;
}
return <PullRequestGrid pullRequestGroups={pullRequestGroups} />;
return <PullRequestGrid pullRequestGroups={pullRequestGroups ?? []} />;
};
const DEFAULT_COLUMN_CONFIGS: PullRequestColumnConfig[] = [
{
title: 'Created by me',
filters: [{ type: FilterType.CreatedByCurrentUser }],
simplified: false,
},
{
title: 'Other PRs',
filters: [{ type: FilterType.All }],
simplified: true,
},
];
type PullRequestsPageProps = {
projectName?: string;
pollingInterval?: number;
defaultColumnConfigs?: PullRequestColumnConfig[];
};
export const PullRequestsPage = ({
projectName,
pollingInterval,
defaultColumnConfigs,
}: PullRequestsPageProps) => {
const { pullRequests, loading, error } = useDashboardPullRequests(
projectName,
pollingInterval,
);
const userEmail = useUserEmail();
const pullRequestGroupConfigs = usePullRequestGroupConfigs(userEmail);
const pullRequestGroups = usePullRequestGroups(
const [columnConfigs] = useState(
defaultColumnConfigs ?? DEFAULT_COLUMN_CONFIGS,
);
const filterProcessor = useFilterProcessor();
const pullRequestGroupConfigs = getPullRequestGroupConfigs(
columnConfigs,
filterProcessor,
);
const pullRequestGroups = getPullRequestGroups(
pullRequests,
pullRequestGroupConfigs,
);
@@ -15,3 +15,17 @@
*/
export { PullRequestsPage } from './PullRequestsPage';
export type { PullRequestColumnConfig } from './lib/types';
export { FilterType } from './lib/filters';
export type {
BaseFilter,
Filter,
PullRequestFilter,
AssignedToUserFilter,
CreatedByUserFilter,
AssignedToTeamFilter,
CreatedByTeamFilter,
AssignedToTeamsFilter,
CreatedByTeamsFilter,
AllFilter,
} from './lib/filters';
@@ -0,0 +1,27 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
export type AllFilter = BaseFilter & {
type: FilterType.All;
};
export function createAllFilter(): PullRequestFilter {
return (_pullRequest: DashboardPullRequest): boolean => true;
}
@@ -0,0 +1,39 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { stringArrayHas } from '../../../../utils';
export type AssignedToTeamFilter = BaseFilter & {
type: FilterType.AssignedToTeam;
teamId: string;
};
export function createAssignedToTeamFilter(
filter: AssignedToTeamFilter,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean => {
const reviewerIds = pullRequest.reviewers?.map(reviewer => reviewer.id);
if (!reviewerIds) {
return false;
}
return stringArrayHas(reviewerIds, filter.teamId, true);
};
}
@@ -0,0 +1,51 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createAssignedToTeamFilter } from './assignedToTeamFilter';
export type AssignedToTeamsFilter = BaseFilter &
(
| {
type: FilterType.AssignedToTeams;
teamIds: string[];
}
| {
type: FilterType.AssignedToCurrentUsersTeams;
teamIds?: string[];
}
);
export function createAssignedToTeamsFilter(
filter: AssignedToTeamsFilter,
): PullRequestFilter {
const teamIds = filter.teamIds;
return (pullRequest: DashboardPullRequest): boolean => {
if (!teamIds) {
return false;
}
return teamIds.some(teamId => {
return createAssignedToTeamFilter({
type: FilterType.AssignedToTeam,
teamId,
})(pullRequest);
});
};
}
@@ -0,0 +1,50 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { stringArrayHas } from '../../../../utils';
export type AssignedToUserFilter = BaseFilter &
(
| {
type: FilterType.AssignedToUser;
email: string;
}
| {
type: FilterType.AssignedToCurrentUser;
email?: string;
}
);
export function createAssignedToUserFilter(
filter: AssignedToUserFilter,
): PullRequestFilter {
const email = filter.email;
return (pullRequest: DashboardPullRequest): boolean => {
const uniqueNames = pullRequest.reviewers?.map(
reviewer => reviewer.uniqueName,
);
if (!email || !uniqueNames) {
return false;
}
return stringArrayHas(uniqueNames, email, true);
};
}
@@ -0,0 +1,150 @@
/*
* 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 { Filter, FilterType } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createFilter } from './createFilter';
describe('createFilter', () => {
const pullRequest = {
createdBy: {
uniqueName: 'user1@backstage.com',
teamIds: ['team1Id', 'team2Id'],
},
reviewers: [
{ uniqueName: 'user2@backstage.com' },
{ id: 'team2Id' },
{ id: 'team3Id' },
],
} as DashboardPullRequest;
const testCases: Array<{ filter: Filter; result: boolean }> = [
{
filter: {
type: FilterType.AssignedToUser,
email: 'user2@backstage.com',
},
result: true,
},
{
filter: {
type: FilterType.AssignedToUser,
email: 'random-user@backstage.com',
},
result: false,
},
{
filter: {
type: FilterType.CreatedByUser,
email: 'user1@backstage.com',
},
result: true,
},
{
filter: {
type: FilterType.CreatedByUser,
email: 'random-user@backstage.com',
},
result: false,
},
{
filter: {
type: FilterType.AssignedToTeam,
teamId: 'team2Id',
},
result: true,
},
{
filter: {
type: FilterType.AssignedToTeam,
teamId: 'randomTeamId',
},
result: false,
},
{
filter: {
type: FilterType.CreatedByTeam,
teamId: 'team1Id',
},
result: true,
},
{
filter: {
type: FilterType.CreatedByTeam,
teamId: 'randomTeamId',
},
result: false,
},
{
filter: {
type: FilterType.AssignedToTeams,
teamIds: ['team2Id', 'randomTeamId'],
},
result: true,
},
{
filter: {
type: FilterType.AssignedToTeams,
teamIds: ['team2Id', 'team3Id'],
},
result: true,
},
{
filter: {
type: FilterType.AssignedToTeams,
teamIds: ['randomTeam1Id', 'randomTeam2Id'],
},
result: false,
},
{
filter: {
type: FilterType.CreatedByTeams,
teamIds: ['team1Id', 'randomTeamId'],
},
result: true,
},
{
filter: {
type: FilterType.CreatedByTeams,
teamIds: ['team1Id', 'team2Id'],
},
result: true,
},
{
filter: {
type: FilterType.CreatedByTeams,
teamIds: ['randomTeam1Id', 'randomTeam2Id'],
},
result: false,
},
{
filter: {
type: FilterType.All,
},
result: true,
},
];
testCases.forEach(({ filter, result }) => {
it(`should return ${String(result)} when pull request ${
result ? 'is' : 'is not'
} ${filter.type}`, () => {
const pullRequestFilter = createFilter(filter);
expect(pullRequestFilter(pullRequest)).toBe(result);
});
});
});
@@ -0,0 +1,71 @@
/*
* 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 { Filter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createAllFilter } from './allFilter';
import { createAssignedToTeamFilter } from './assignedToTeamFilter';
import { createAssignedToTeamsFilter } from './assignedToTeamsFilter';
import { createAssignedToUserFilter } from './assignedToUserFilter';
import { createCreatedByTeamFilter } from './createdByTeamFilter';
import { createCreatedByTeamsFilter } from './createdByTeamsFilter';
import { createCreatedByUserFilter } from './createdByUserFilter';
export function createFilter(filters: Filter | Filter[]): PullRequestFilter {
const mapFilter = (filter: Filter): PullRequestFilter => {
switch (filter.type) {
case FilterType.AssignedToUser:
case FilterType.AssignedToCurrentUser:
return createAssignedToUserFilter(filter);
case FilterType.CreatedByUser:
case FilterType.CreatedByCurrentUser:
return createCreatedByUserFilter(filter);
case FilterType.AssignedToTeam:
return createAssignedToTeamFilter(filter);
case FilterType.CreatedByTeam:
return createCreatedByTeamFilter(filter);
case FilterType.AssignedToTeams:
case FilterType.AssignedToCurrentUsersTeams:
return createAssignedToTeamsFilter(filter);
case FilterType.CreatedByTeams:
case FilterType.CreatedByCurrentUsersTeams:
return createCreatedByTeamsFilter(filter);
case FilterType.All:
return createAllFilter();
default:
return _ => false;
}
};
if (Array.isArray(filters)) {
if (filters.length === 1) {
return mapFilter(filters[0]);
}
return (pullRequest: DashboardPullRequest): boolean =>
filters.every(filter => mapFilter(filter)(pullRequest));
}
return mapFilter(filters);
}
@@ -0,0 +1,42 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { stringArrayHas } from '../../../../utils';
export type CreatedByTeamFilter = BaseFilter &
({
type: FilterType.CreatedByTeam;
} & ({ teamId: string } | { teamName: string }));
export function createCreatedByTeamFilter(
filter: CreatedByTeamFilter,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean => {
const [createdByTeams, team] =
'teamId' in filter
? [pullRequest.createdBy?.teamIds, filter.teamId]
: [pullRequest.createdBy?.teamNames, filter.teamName];
if (!createdByTeams) {
return false;
}
return stringArrayHas(createdByTeams, team, true);
};
}
@@ -0,0 +1,65 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createCreatedByTeamFilter } from './createdByTeamFilter';
export type CreatedByTeamsFilter = BaseFilter &
(
| ({
type: FilterType.CreatedByTeams;
} & ({ teamIds: string[] } | { teamNames: string[] }))
| {
type: FilterType.CreatedByCurrentUsersTeams;
teamIds?: string[];
}
);
export function createCreatedByTeamsFilter(
filter: CreatedByTeamsFilter,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean => {
if ('teamNames' in filter) {
const teamNames = filter.teamNames;
if (!teamNames) {
return false;
}
return teamNames.some(teamName => {
return createCreatedByTeamFilter({
type: FilterType.CreatedByTeam,
teamName,
})(pullRequest);
});
}
const teamIds = filter.teamIds;
if (!teamIds) {
return false;
}
return teamIds.some(teamId => {
return createCreatedByTeamFilter({
type: FilterType.CreatedByTeam,
teamId,
})(pullRequest);
});
};
}

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