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

# Conflicts:
#	packages/app/package.json
This commit is contained in:
Karan Shah
2022-01-05 09:36:05 +00:00
395 changed files with 4339 additions and 2302 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-splunk-on-call': patch
---
Clean up emptystate.svg image, removing wrong white artifact from the background
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Bump `testcontainers` dependency to version `8.1.2`
+3
View File
@@ -124,6 +124,9 @@ jobs:
- name: check api reports and generate API reference
run: yarn build:api-reports:only --ci --docs
- name: verify changesets
run: node scripts/verify-changesets.js
- name: verify doc links
run: node scripts/verify-links.js
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
for your contributions.
days-before-issue-stale: 60
days-before-issue-close: 7
exempt-issue-labels: 'pinned,security,plugin,help wanted,good first issue,rfc'
exempt-issue-labels: 'pinned,security,plugin,help wanted,good first issue,rfc,will-fix'
stale-issue-label: stale
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
@@ -25,6 +25,6 @@ jobs:
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: 5
exempt-pr-labels: reviewer-approved,awaiting-review
exempt-pr-labels: reviewer-approved,awaiting-review,will-fix
stale-pr-label: stale
operations-per-run: 100
-4
View File
@@ -2,10 +2,6 @@
# [Backstage](https://backstage.io)
> 🎄 The maintainers will be taking a break over the holidays from week beginning 20th Dec. The Repository and Discord may be quieter than usual. We will be back next year, rested and restored, on Jan. 3. 🎄
> 🎅 Happy holidays and a Happy New Year to all, and especially those that have made this year special for Backstage! 🎅
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects)
[![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22)
@@ -129,8 +129,12 @@ function msUntilExpiry(token: string): number {
// Calls the specified url regularly using an auth token to set a token cookie
// to authorize regular HTTP requests when loading techdocs
async function setTokenCookie(url: string, getIdToken: () => Promise<string>) {
const token = await getIdToken();
async function setTokenCookie(url: string, identityApi: IdentityApi) {
const { token } = await identityApi.getCredentials();
if (!token) {
return;
}
await fetch(url, {
mode: 'cors',
credentials: 'include',
@@ -138,11 +142,12 @@ async function setTokenCookie(url: string, getIdToken: () => Promise<string>) {
Authorization: `Bearer ${token}`,
},
});
// Call this function again a few minutes before the token expires
const ms = msUntilExpiry(token) - 4 * 60 * 1000;
setTimeout(
() => {
setTokenCookie(url, getIdToken);
setTokenCookie(url, identityApi);
},
ms > 0 ? ms : 10000,
);
@@ -160,16 +165,13 @@ const app = createApp({
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
onResult={async result => {
// When logged in, set a token cookie
if (typeof result.getIdToken !== 'undefined') {
setTokenCookie(
await discoveryApi.getBaseUrl('cookie'),
result.getIdToken,
);
}
// Forward results
props.onResult(result);
onSignInSuccess={async (identityApi: IdentityApi) => {
setTokenCookie(
await discoveryApi.getBaseUrl('cookie'),
identityApi,
);
props.onSignInSuccess(identityApi);
}}
/>
);
@@ -4,7 +4,7 @@ ExampleFetchComponent.tsx reference
```tsx
import React from 'react';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import Alert from '@material-ui/lab/Alert';
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
import { Table, TableColumn, Progress } from '@backstage/core-components';
@@ -35,9 +35,11 @@ We follow this structure for plugin packages (where `x` is the plugin name, for
example `catalog` or `techdocs`):
- `x`: Contains the main frontend code of the plugin.
- `x-module-<name>`: Contains optional modules related to the frontend plugin
package.
- `x-backend`: Contains the main backend code of the plugin.
- `x-backend-module-<name>`: Contains optional modules related to the backend
plugin.
plugin package.
- `x-react`: Contains shared widgets, hooks and similar that both the plugin
itself (`x`) and third-party frontend plugins can depend on.
- `x-node`: Contains utilities for backends that both the plugin backend itself
@@ -0,0 +1 @@
<mxfile host="Chrome" modified="2021-12-30T23:10:43.723Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" etag="XmQE_awyVPeS9B3bWnM_" version="15.5.6" type="device"><diagram id="M4OCM2KiCGRnt6vHj1W_" name="Page-1">7V3bktq4Fv0aHknhCwYe06Q7kzmdOZ2kp2ZyXk4JW4AntkXZopvO149sZLAt+YKvEiFVU4OFcdtaa2svSXtvj7Sle/jog932M7KgM1In1mGkfRipqqLMDfK/sOXt2DIPj8KGjW9b9KRzwzf7J6SNE9q6ty0YpE7ECDnY3qUbTeR50MSpNuD76DV92ho56b+6AxvINHwzgcO2/mVbeEtb1fnk/MVv0N5s6Z82DPrFCpg/Nj7ae/TvjVRtHf07fu2C+Fr0/GALLPSaaNLuR9rSRwgfP7mHJXTCvo277fi7h5xvT/ftQw9X+cHBgb9bB924/+ur+fXpXg2e/sFjZaodr/MCnD2MHyS6XfwWd1H0kDC8zGSk3b1ubQy/7YAZfvtKSEHatth1yJFCPgLH3njkswPX5LbuXqCPbdLb72kzRuH5a9txlshBPmnxkEeudMc+Dn3C8ArwkGiij/cRIhdi/42cEn+7oF1NqTiNOfaaAHZC27YJTBWNNgJKps3p2ucOJR9on17Qvwud6U1oEf7RQ/rs6Q5GPt6iDfKA84jC3oq69R+I8Ru1HrDHKN3p8GDjvxOfv4eXejelRx8O9MrRwVt84JEH/Dt5kPhVeHj+WXQU/26NPBxjRzg/if5FZ1nvQ3skzSsHmT+et7Z3bH6wnfhGAwx8TB9DP36bOMrlQID2vkn7788v8AG9HVYfPj/O0cvy95+fNto4HjmAv4G44DzK9hCDQkb50AHYfkmPEa2zQ4kJmkOPMxPuz63tseXMkO9JgnDZ0iHq5t5/iR5HaZsC1p+H5fjnf9RP31+8P/bgiwV/fh+rhmAcYAfccg6k+qwjQiiFhLj24YPLndlMLO7EXBaCO7Pqo0keeWbXwh4uXDN1KPYU3XZC+YHdrpn420HfJjcH/fAU29vQX2VFoGtbVkTFjFgMjr96jI4+zM8t5EjNyEaCvgXgfG1GwProB0x8Y5hzuFqfvomFfaGDqa4zMzJT1VmZOeeozM5EpqJoAg0DzUcBKQcBtaILmeqCuZB5Iwmq9DdhkQV27nmDCYf/Pn9Z6v9b2IuvfzyZS//ZmE2+xEN//fGiPaFQVWW+U6epUaLCGPEAXNsJG57BFrmAttIbVJQ+KJXBu9uhRR9STaiMmggXyEifyKQo1uu1anIVhWWsjKnRk6LQJ0MrCs4qoGQTi8x4ocioKTThJxZ89miNp6UDr4GqUkrQqnSZikaXmTB0UavypYYckZYvymArpkW3nVAaO2e/sb3xSDWAG+oHJ3T9tJFcKm7e4FN3ySFHLl7g0GKNSx9PLaBeC7JFnZWvhCizfpdCGo8lXQmXX2g5fXbJhEUgP1S8WdunH5rV9ENSylwOXQTzQzl8mQrDl3oyV87tlqp0GU7mFt12Tdky/hVWUwaWL7xll87kS5FDTHDkLoZ98mivfODbMGAoELzargOiSKKkzU0SAJqkl6DPgfoYj9VCZ56CreLOXLCdeYqFSwVfTbvqzSnTmyM9CtsjvUSGBPWB2tTYRK6LvCszLRbTQspVt5o0zhrHZrpaq+Q+gFEVZQwDPN5j22Et6IZ0BaRnQyPNetA8pEHw4wZyLZAVXsxsryjPGZSXdHgW3QUqEz2zHmJU9IEn22q9NxdlNmMCDBy0GZuOHT65REZjTeHc0nlGM1dXmnGB0cwvRTprNByt06/RxFabjzN+23Hs5gavDBJHYUP1slaMvLW9ucErpa6JI8Dz4YW+j/yb9UqqaBROjhXfCbvH9LobzDVgng4NMy+VLg2zTRToxgfYlmuxQRyQh3fFKrvc8OAjgmu0Rvc+DKCfLJEPGXyFmKGUCxv+Ep3SWXeysz1G2fhwDHY78p8tldGU791XNJoT5aTVr2rpLDQE2IJrsHewXDJHHJT7lLE5sRcMcr0ms160I0oOnmKmxG2p/UnO7mX5luflqaz8RTxOZFaRPxVlQ1y9EaA7AvC7fCgCFN52UTGJYAt24UfSU87bnR+u2OPyUT2dcLR27N1vowvqTQRUbEWkaEFHLeYZXcoRUpyhd7robuxd1LC9buLefonIlMLcPoFDU3IimeqM3ANHvqUJI3zEfmXCiBUpWXjbDWOZwuWuvQMzP6KNMsdtix74pBiZCSInbrvvFPY6acji+i85h6OZ+KGVOROPQesoXZRC1FchpUSIuK4bSXaWxYhHR9m5UQ+kGi6LsfC+6zq5a3VutZKSAjLLM7f0xjYOCAL6+VQVMbaNoVwgJ/a3ZxfI0O0U9/SMkBPiIeSeAhP1pHBWIrnT4TaCnrhdWb7f6ki1lwAVawpnPJNbGDMN8DfgCkkmw1ZCjqsXtdZRxfy+k/eVI7+vMBFL3GlrDncaJ/gNWyfrWrkjmBwsvO+EU7k/EAdAwCetT5EIHAkfojybZhauq0Yot5Klk1OCuprORngL/XGe2vYhMOWKXq6zc1zMzAskW5lTV3llsrtz6hXXE4spIF8WV63Aqm44wNPs/XKgYlmPYg54yGJDrwRmQK3En24YwIuT7ZcCxqABJJct+w5Q9bxwJVa64gmTOhvWg0zdhCp9X5kEUszHGhfQGLhucSJOQYbqK81GEMEmZPF9JwSDLLOwqVY+C+Muq7UyC+MXg2cTCC/a7bjG+Vebe/TFFG5x5nba2GhbsnFpwwZ2X8Saq5yyiU8bXqh4r7Rhsz4uos0VzvLEJw03j7Ir1hQKkMLw4nz/niZIauM5tSOd3Kq2QLAtF+gXbKdltp05gcM61/F3ViBCL0xoOymq3nLaipGvTd7Bi1Hp1TLd6JD3yya76dInu+mlyW5yFqISCOPBKzbMSkvLWfDlBrHcVRvYTamEY+x3haElp6hOB/SK3IlAafG+RMmEq1xjqGhPilR13bhQlw6Zkf4xkbtDHumMaxs4q803+qLD4CpJZUfXrEraQleqWb5A1j58LQCtQirwhoC5Y7skb1KtTkzkedDEYBVfYXJJp3BfB89bwGjldfD8aoycQbARwytlPyd5St84X1FinGEsZ+JwvTrnzLiufdxoDFf1dRIOlmpnBsJuQ8kEZYvhPI2h5EZ09Qklp2ilRFC2GJvXGEpeem6vUKoMlMk1YiLaIsmWAZc8K04jmO5K6oo43qnEq50Q5hEmTanU+j3LgO4Q1ErHVaVfCFk99ilALvJ3W9u8gVgJRN7bzfoFsej9IDcEyxHkzYu6QpD7CHIr1RbzEFtQqv05xZwIVgbL5eOnmyFW9IdDW6Kiyz3VqDVrrJH+m1lD4VRZ7hk2uacVtWaIjWEzepzX58DGTiFkgq3WbLC5tc0Hh63CMq7IsPUzSBrCDZIGg5KYbyYNgxofgGs7YcMz2CIX0Fb6ZxRlJGjexElAlKdexVZUmjkR022RIVQb9ddzbo2zDyeRgffkTg3h3Gnjcq83Ay83cKWygVdNrBvAwNvecrxK4ZX14MMLr5kkBt6oAqa0I0N11z8VdmSIVaoAFCupmdmoSri0HKvufcTlmN64rO/Aw9hVM0y9AoYZ7MLSErk7gO2V7dj4jeGfBMvwydS/SQvyhoGDk3am8Eq9dKdvjAo7YXEHP4IVdJ5QYEcvziOmgjBGLgeBY7hbEln6shH3sCEwbN+tQGCb785Dyv+xbwNvw6ktUi1ct5r15++WpbM+FVb+nnbcfIQBffxxGMrZkuxVSmVv/GKRVCjf4mJajMKIEYQT330MEfkcvfBSu/8X</diagram></mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

+59 -2
View File
@@ -39,6 +39,7 @@ kubernetes:
region: 'europe-west1'
skipTLSVerify: true
skipMetricsLookup: true
exposeDashboard: true
```
### `serviceLocatorMethod`
@@ -113,9 +114,13 @@ kubectl -n <NAMESPACE> get secret $(kubectl -n <NAMESPACE> get sa <SERVICE_ACCOU
Specifies the link to the Kubernetes dashboard managing this cluster.
Note that you should specify the app used for the dashboard using the
**dashboardApp property**, in order to properly format links to kubernetes
`dashboardApp` property, in order to properly format links to kubernetes
resources, otherwise it will assume that you're running the standard one.
Note also that this attribute is optional for some kinds of dashboards, such as
GKE, which requires additional parameters specified in the `dashboardParameters`
option.
##### `clusters.\*.dashboardApp` (optional)
Specifies the app that provides the Kubernetes dashboard.
@@ -124,11 +129,14 @@ This will be used for formatting links to kubernetes objects inside the
dashboard.
The supported dashboards are: `standard`, `rancher`, `openshift`, `gke`, `aks`,
`eks` However, not all of them are implemented yet, so please contribute!
`eks`. However, not all of them are implemented yet, so please contribute!
Note that it will default to the regular dashboard provided by the Kubernetes
project (`standard`), that can run in any Kubernetes cluster.
Note that for the `gke` app, you must provide additional information in the
`dashboardParameters` option.
Note that you can add your own formatter by registering it to the
`clusterLinksFormatters` dictionary, in the app project.
@@ -143,6 +151,43 @@ See also
https://github.com/backstage/backstage/tree/master/plugins/kubernetes/src/utils/clusterLinks/formatters
for real examples.
##### `clusters.\*.dashboardParameters` (optional)
Specifies additional information for the selected `dashboardApp` formatter.
Note that, even though `dashboardParameters` is optional, it might be mandatory
for some dashboards, such as GKE.
###### required parameters for GKE
| Name | Description |
| ----------- | ------------------------------------------------------------------------ |
| projectId | the ID of the GCP project containing your Kubernetes clusters |
| region | the region of GCP containing your Kubernetes clusters |
| clusterName | the name of your kubernetes cluster, within your `projectId` GCP project |
Note that the GKE cluster locator can automatically provide the values for the
`dashboardApp` and `dashboardParameters` options if you set the
`exposeDashboard` property to `true`.
Example:
```yaml
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- url: http://127.0.0.1:9999
name: my-cluster
dashboardApp: gke
dashboardParameters:
projectId: my-project
region: us-east1
clusterName: my-cluster
```
##### `clusters.\*.caData` (optional)
PEM-encoded certificate authority certificates.
@@ -184,6 +229,10 @@ For example:
Will configure the Kubernetes plugin to connect to all GKE clusters in the
project `gke-clusters` in the region `europe-west1`.
Note that the GKE cluster locator can automatically provide the values for the
`dashboardApp` and `dashboardParameters` options if you enable the
`exposeDashboard` option.
##### `projectId`
The Google Cloud project to look for Kubernetes clusters in.
@@ -203,6 +252,14 @@ presented by the API server. Defaults to `false`.
This determines whether the Kubernetes client looks up resource metrics
CPU/Memory for pods returned by the API server. Defaults to `false`.
##### `exposeDashboard`
This determines wether the `dashboardApp` and `dashboardParameters` should be
automatically configured in order to expose the GKE dashboard from the
Kubernetes plugin.
Defaults to `false`.
### `customResources` (optional)
Configures which [custom resources][3] to look for when returning an entity's
@@ -10,8 +10,7 @@ and find catalog entities. This is already set up by default by
`@backstage/create-app`.
If you want to change the default index page - such as to add a custom filter to
the catalog - you can replace the routing in `App.tsx` to point to your own
`CatalogIndexPage`.
the catalog - you can create your own `CatalogIndexPage`.
> Note: The catalog index page is designed to have a minimal code footprint to
> support easy customization, but creating a copy does introduce a possibility
@@ -21,23 +20,25 @@ the catalog - you can replace the routing in `App.tsx` to point to your own
For example, suppose that I want to allow filtering by a custom annotation added
to entities, `company.com/security-tier`. To start, I'll copy the code for the
default catalog page and create a component in a
[new plugin](../../plugins/create-a-plugin.md):
default catalog page and create a component.
```tsx
// imports, etc omitted for brevity. for full source see:
// https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
// https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx
export const CustomCatalogPage = ({
columns,
actions,
initiallySelectedFilter = 'owned',
}: CatalogPageProps) => {
const createComponentLink = useRouteRef(
catalogPlugin.externalRoutes.createComponent,
);
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<EntityListProvider>
<Content>
<ContentHeader titleComponent={<CatalogKindHeader />}>
<CreateButton title="Create Component" to={link} />
<CreateButton title="Create Component" to={createComponentLink()} />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<FilteredEntityLayout>
@@ -167,21 +168,7 @@ export const CustomCatalogPage = ({
};
```
This page itself can be exported as a routable extension in the plugin:
```ts
export const CustomCatalogIndexPage = myPlugin.provide(
createRoutableExtension({
name: 'CustomCatalogIndexPage',
component: () =>
import('./components/CustomCatalogPage').then(m => m.CustomCatalogPage),
mountPoint: catalogRouteRef,
}),
);
```
Finally, we can replace the catalog route in the Backstage application with our
new `CustomCatalogIndexPage`.
Finally, we can apply our new `CustomCatalogPage`.
```diff
# packages/app/src/App.tsx
@@ -189,7 +176,9 @@ const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
- <Route path="/catalog" element={<CatalogIndexPage />} />
+ <Route path="/catalog" element={<CustomCatalogIndexPage />} />
+ <Route path="/catalog" element={<CatalogIndexPage />}>
+ <CustomCatalogPage />
+ </Route>
```
The same method can be used to customize the _default_ filters with a different
@@ -22,6 +22,13 @@ yarn backstage-cli versions:bump
The reason for bumping all `@backstage` packages at once is to maintain the
dependencies that they have between each other.
If you are using other plugins you can pass in the `--pattern` option to update
more than just the `@backstage/*` dependencies.
```bash
yarn backstage-cli versions:bump --pattern '@{backstage,roadiehq}/*'
```
## Following create-app template changes
The `@backstage/create-app` command creates the initial structure of your
+16 -4
View File
@@ -20,8 +20,7 @@ To use this integration, add configuration to your `app-config.yaml`:
```yaml
integrations:
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
- accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
```
@@ -35,8 +34,21 @@ instruct the AWS S3 reader to assume a role before accessing S3:
```yaml
integrations:
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
- accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
roleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/example-role'
```
Configuration allows specifying custom S3 endpoint, along with
[path-style access](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html)
to support integration with providers like
[LocalStack](https://github.com/localstack/localstack):
```yaml
integrations:
awsS3:
- endpoint: 'http://localhost:4566'
s3ForcePathStyle: true
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
```
+1
View File
@@ -594,6 +594,7 @@ Usage: backstage-cli versions:bump [options]
Options:
-h, --help display help for command
-p, --pattern Override glob for matching packages to upgrade
```
## versions:check
+100 -2
View File
@@ -164,12 +164,110 @@ https://circleci.com.
![CircleCI plugin talking to proxy talking to SaaS Circle CI](../assets/architecture-overview/circle-ci-plugin-architecture.png)
## Package Architecture
Backstage relies heavily on NPM packages, both for distribution of libraries,
and structuring of code within projects. While the way you structure your
Backstage project is up to you, there is a set of established patterns that we
encourage you to follow. These patterns can help set up a sound project
structure as well as provide familiarity between different Backstage projects.
The following diagram shows an overview of the package architecture of
Backstage. It takes the point of view of an individual plugin and all of the
packages that it may contain, indicated by the thicker border and italic text.
Surrounding the plugin are different package groups which are the different
possible interface points of the plugin. Note that not all library package lists
are complete as packages have been omitted for brevity.
![Package architecture](../assets/architecture-overview/package-architecture.png)
<!--
NOTE: The diagram is exported from /docs/assets/architecture-overview/package-architecture.drawio
It can be edited using https://www.diagrams.net/
Export it as a PNG with 10px margin without transparent background
-->
### Overview
The arrows in the diagram above indicate a runtime dependency on the code of the
target package. This strict dependency graph only applies to runtime
`dependencies`, and there may be `devDependencies` that break the rules of this
table for the purpose of testing. While there are some arrows that show a
dependency on a collection of frontend, backend and isomorphic packages, those
still have to abide by important compatibility rules shown in the bottom left.
The `app` and `backend` packages are the entry points of a Backstage project.
The `app` package is the frontend application that brings together a collection
of frontend plugins and customizes them to fit an organization, while the
`backend` package is the backend service that powers the Backstage application.
Worth noting is that there can be more than one instance of each of these
packages within a project. Particularly the `backend` packages can benefit from
being split up into smaller deployment units that each serve their own purpose
with a smaller collection of plugins.
### Plugin Packages
A typical plugin consists of up to five packages, two frontend ones, two
backend, and one isomorphic packages. All packages within the plugin must share
a common prefix, typically of the form `@<scope>/plugin-<plugin-id>`, but
alternatives like `backstage-plugin-<plugin-id>` or
`@scope/backstage-plugin-<plugin-id>` are also valid. Along with this prefix,
each of the packages have their own unique suffix that denotes their role. In
addition to these five plugin packages it's also possible for to a plugin to
have additional frontend and backend modules that can be installed to enable
optional features. For a full list of suffixes and their roles, see the
[Plugin Package Structure ADR](../architecture-decisions/adr011-plugin-package-structure.md).
The `-react`, `-common`, and `-node` plugin packages together form the external
library of a plugin. The plugin library enables other plugins to build on top of
and extend a plugin, and likewise allows the plugin to depend on and extend
other plugins. Because of this, it is preferable that plugin library packages
allow duplicate installations of themselves, as you may end up with a mix of
versions being installed as dependencies of various plugins. It is also
forbidden for plugins to directly import non-library packages from other
plugins, all communication between plugins must be handled through libraries and
the application itself.
### Frontend Packages
The frontend packages are grouped into two main groups. The first one is
"Frontend App Core", which is the set of packages that are only used by the
`app` package itself. These packages help build up the core structure of the app
as well as provide a foundation for the plugin libraries to rely upon.
The second group is the rest of the shared packages, further divided into
"Frontend Plugin Core" and "Frontend Libraries". The core packages that are
considered particularly stable and form the core of the frontend framework.
Their most important role is to form the boundary around each plugin and provide
a set of tools that helps you combine a collection of plugins into a running
application. The rest of the frontend packages are more traditional libraries
that serve as building blocks to create plugins.
### Backend Packages
The backend library packages do not currently share a similar plugin
architecture as the frontend packages. They are instead simply a collection of
building blocks and patterns that help you build backend services. This is
however likely to change in the future.
### Common Packages
The common packages are the packages are effectively depended on by all other
pages. This is a much smaller set of packages but they are also very pervasive.
Because the common packages are isomorphic and must execute both in the frontend
and backend, they are never allowed to depend on any of the frontend of backend
packages.
The Backstage CLI is in a category of its own and is depended on by virtually
all other packages. It's not a library in itself though, and must always be a
development dependency only.
## Databases
As we have seen, both the `lighthouse-audit-service` and `catalog-backend`
require a database to work with.
The Backstage backend and its builtin plugins are based on the
The Backstage backend and its built-in plugins are based on the
[Knex](http://knexjs.org/) library, and set up a separate logical database per
plugin. This gives great isolation and lets them perform migrations and evolve
separate from each other.
@@ -184,7 +282,7 @@ yet.
## Cache
The Backstage backend and its builtin plugins are also able to leverage cache
The Backstage backend and its built-in plugins are also able to leverage cache
stores as a means of improving performance or reliability. Similar to how
databases are supported, plugins receive logically separated cache connections,
which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood.
+1 -1
View File
@@ -135,7 +135,7 @@ changes, let's start by wiping this component clean.
```tsx
import React from 'react';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import Alert from '@material-ui/lab/Alert';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
+44
View File
@@ -1,5 +1,49 @@
# example-app
## 0.2.59
### Patch Changes
- Updated dependencies
- @backstage/cli@0.10.5
- @backstage/plugin-search@0.5.4
- @backstage/plugin-techdocs@0.12.13
- @backstage/core-plugin-api@0.4.1
- @backstage/plugin-catalog-react@0.6.10
- @backstage/plugin-kubernetes@0.5.3
- @backstage/core-app-api@0.3.1
- @backstage/core-components@0.8.3
- @backstage/plugin-apache-airflow@0.1.2
- @backstage/plugin-azure-devops@0.1.9
- @backstage/plugin-badges@0.2.18
- @backstage/plugin-catalog@0.7.7
- @backstage/plugin-catalog-graph@0.2.5
- @backstage/plugin-catalog-import@0.7.8
- @backstage/plugin-circleci@0.2.33
- @backstage/plugin-cloudbuild@0.2.31
- @backstage/plugin-code-coverage@0.1.21
- @backstage/plugin-cost-insights@0.11.16
- @backstage/plugin-explore@0.3.24
- @backstage/plugin-gcp-projects@0.3.12
- @backstage/plugin-github-actions@0.4.30
- @backstage/plugin-graphiql@0.2.26
- @backstage/plugin-home@0.4.9
- @backstage/plugin-jenkins@0.5.16
- @backstage/plugin-kafka@0.2.24
- @backstage/plugin-lighthouse@0.2.33
- @backstage/plugin-newrelic@0.3.12
- @backstage/plugin-newrelic-dashboard@0.1.2
- @backstage/plugin-org@0.3.33
- @backstage/plugin-pagerduty@0.3.21
- @backstage/plugin-rollbar@0.3.22
- @backstage/plugin-scaffolder@0.11.17
- @backstage/plugin-sentry@0.3.32
- @backstage/plugin-shortcuts@0.1.18
- @backstage/plugin-tech-insights@0.1.4
- @backstage/plugin-tech-radar@0.5.1
- @backstage/plugin-user-settings@0.3.15
- @backstage/plugin-api-docs@0.6.21
## 0.2.58
### Patch Changes
+42 -42
View File
@@ -1,61 +1,61 @@
{
"name": "example-app",
"version": "0.2.58",
"version": "0.2.59",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.3",
"@backstage/catalog-model": "^0.9.8",
"@backstage/cli": "^0.10.3",
"@backstage/core-app-api": "^0.3.0",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/integration-react": "^0.1.16",
"@backstage/plugin-airbrake": "^0.0.0",
"@backstage/plugin-api-docs": "^0.6.19",
"@backstage/plugin-azure-devops": "^0.1.8",
"@backstage/plugin-apache-airflow": "^0.1.1",
"@backstage/plugin-badges": "^0.2.17",
"@backstage/plugin-catalog": "^0.7.5",
"@backstage/plugin-catalog-graph": "^0.2.4",
"@backstage/plugin-catalog-import": "^0.7.6",
"@backstage/plugin-catalog-react": "^0.6.8",
"@backstage/plugin-circleci": "^0.2.32",
"@backstage/plugin-cloudbuild": "^0.2.30",
"@backstage/plugin-code-coverage": "^0.1.20",
"@backstage/plugin-cost-insights": "^0.11.14",
"@backstage/plugin-explore": "^0.3.23",
"@backstage/plugin-gcp-projects": "^0.3.11",
"@backstage/plugin-github-actions": "^0.4.28",
"@backstage/plugin-graphiql": "^0.2.25",
"@backstage/plugin-home": "^0.4.8",
"@backstage/plugin-jenkins": "^0.5.15",
"@backstage/plugin-kafka": "^0.2.23",
"@backstage/plugin-kubernetes": "^0.5.2",
"@backstage/plugin-lighthouse": "^0.2.32",
"@backstage/plugin-newrelic": "^0.3.11",
"@backstage/plugin-newrelic-dashboard": "^0.1.1",
"@backstage/plugin-org": "^0.3.32",
"@backstage/plugin-pagerduty": "0.3.20",
"@backstage/plugin-rollbar": "^0.3.21",
"@backstage/plugin-scaffolder": "^0.11.15",
"@backstage/plugin-search": "^0.5.2",
"@backstage/plugin-sentry": "^0.3.31",
"@backstage/plugin-shortcuts": "^0.1.16",
"@backstage/plugin-tech-radar": "^0.5.0",
"@backstage/plugin-techdocs": "^0.12.11",
"@backstage/plugin-api-docs": "^0.6.21",
"@backstage/plugin-azure-devops": "^0.1.9",
"@backstage/plugin-apache-airflow": "^0.1.2",
"@backstage/plugin-badges": "^0.2.18",
"@backstage/plugin-catalog": "^0.7.7",
"@backstage/plugin-catalog-graph": "^0.2.5",
"@backstage/plugin-catalog-import": "^0.7.8",
"@backstage/plugin-catalog-react": "^0.6.10",
"@backstage/plugin-circleci": "^0.2.33",
"@backstage/plugin-cloudbuild": "^0.2.31",
"@backstage/plugin-code-coverage": "^0.1.21",
"@backstage/plugin-cost-insights": "^0.11.16",
"@backstage/plugin-explore": "^0.3.24",
"@backstage/plugin-gcp-projects": "^0.3.12",
"@backstage/plugin-github-actions": "^0.4.30",
"@backstage/plugin-graphiql": "^0.2.26",
"@backstage/plugin-home": "^0.4.9",
"@backstage/plugin-jenkins": "^0.5.16",
"@backstage/plugin-kafka": "^0.2.24",
"@backstage/plugin-kubernetes": "^0.5.3",
"@backstage/plugin-lighthouse": "^0.2.33",
"@backstage/plugin-newrelic": "^0.3.12",
"@backstage/plugin-newrelic-dashboard": "^0.1.2",
"@backstage/plugin-org": "^0.3.33",
"@backstage/plugin-pagerduty": "0.3.21",
"@backstage/plugin-rollbar": "^0.3.22",
"@backstage/plugin-scaffolder": "^0.11.17",
"@backstage/plugin-search": "^0.5.4",
"@backstage/plugin-sentry": "^0.3.32",
"@backstage/plugin-shortcuts": "^0.1.18",
"@backstage/plugin-tech-radar": "^0.5.1",
"@backstage/plugin-techdocs": "^0.12.13",
"@backstage/plugin-todo": "^0.1.17",
"@backstage/plugin-user-settings": "^0.3.14",
"@backstage/plugin-user-settings": "^0.3.15",
"@backstage/search-common": "^0.2.0",
"@backstage/plugin-tech-insights": "^0.1.2",
"@backstage/plugin-tech-insights": "^0.1.4",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"@octokit/rest": "^18.5.3",
"@roadiehq/backstage-plugin-github-insights": "^1.2.2",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.11",
"@roadiehq/backstage-plugin-github-insights": "^1.4.2",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.3.2",
"@roadiehq/backstage-plugin-travis-ci": "^1.3.2",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/backend-common
## 0.10.2
### Patch Changes
- 21ae56168e: Updated the Git class with the following:
- Added `depth` and `noCheckout` options to Git clone, using these you can create a bare clone that includes just the git history
- New `log` function which you can use to view the commit history of a git repo
- eacc582473: Reverted the default CSP configuration to include `'unsafe-eval'` again, which was mistakenly removed in the previous version.
## 0.10.1
### Patch Changes
+8 -7
View File
@@ -234,8 +234,13 @@ export class Git {
remote: string;
url: string;
}): Promise<void>;
// (undocumented)
clone(options: { url: string; dir: string; ref?: string }): Promise<void>;
clone(options: {
url: string;
dir: string;
ref?: string;
depth?: number;
noCheckout?: boolean;
}): Promise<void>;
// (undocumented)
commit(options: {
dir: string;
@@ -249,12 +254,10 @@ export class Git {
email: string;
};
}): Promise<string>;
// (undocumented)
currentBranch(options: {
dir: string;
fullName?: boolean;
}): Promise<string | undefined>;
// (undocumented)
fetch(options: { dir: string; remote?: string }): Promise<void>;
// (undocumented)
static fromAuth: (options: {
@@ -264,7 +267,7 @@ export class Git {
}) => Git;
// (undocumented)
init(options: { dir: string; defaultBranch?: string }): Promise<void>;
// (undocumented)
log(options: { dir: string; ref?: string }): Promise<ReadCommitResult[]>;
merge(options: {
dir: string;
theirs: string;
@@ -280,9 +283,7 @@ export class Git {
}): Promise<MergeResult>;
// (undocumented)
push(options: { dir: string; remote: string }): Promise<PushResult>;
// (undocumented)
readCommit(options: { dir: string; sha: string }): Promise<ReadCommitResult>;
// (undocumented)
resolveRef(options: { dir: string; ref: string }): Promise<string>;
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.10.1",
"version": "0.10.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -81,7 +81,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.10.4",
"@backstage/cli": "^0.10.5",
"@backstage/test-utils": "^0.2.1",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
@@ -320,4 +320,21 @@ describe('Git', () => {
});
});
});
describe('log', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const ref = 'as43bd7';
const git = Git.fromAuth({});
await git.log({ dir, ref });
expect(isomorphic.log).toHaveBeenCalledWith({
fs,
dir,
ref,
});
});
});
});
+24 -7
View File
@@ -76,12 +76,15 @@ export class Git {
return git.commit({ fs, dir, message, author, committer });
}
/** https://isomorphic-git.org/docs/en/clone */
async clone(options: {
url: string;
dir: string;
ref?: string;
depth?: number;
noCheckout?: boolean;
}): Promise<void> {
const { url, dir, ref } = options;
const { url, dir, ref, depth, noCheckout } = options;
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
return git.clone({
fs,
@@ -90,7 +93,8 @@ export class Git {
dir,
ref,
singleBranch: true,
depth: 1,
depth: depth ?? 1,
noCheckout,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
@@ -99,7 +103,7 @@ export class Git {
});
}
// https://isomorphic-git.org/docs/en/currentBranch
/** https://isomorphic-git.org/docs/en/currentBranch */
async currentBranch(options: {
dir: string;
fullName?: boolean;
@@ -110,7 +114,7 @@ export class Git {
>;
}
// https://isomorphic-git.org/docs/en/fetch
/** https://isomorphic-git.org/docs/en/fetch */
async fetch(options: { dir: string; remote?: string }): Promise<void> {
const { dir, remote = 'origin' } = options;
this.config.logger?.info(
@@ -138,7 +142,7 @@ export class Git {
});
}
// https://isomorphic-git.org/docs/en/merge
/** https://isomorphic-git.org/docs/en/merge */
async merge(options: {
dir: string;
theirs: string;
@@ -180,7 +184,7 @@ export class Git {
});
}
// https://isomorphic-git.org/docs/en/readCommit
/** https://isomorphic-git.org/docs/en/readCommit */
async readCommit(options: {
dir: string;
sha: string;
@@ -189,12 +193,25 @@ export class Git {
return git.readCommit({ fs, dir, oid: sha });
}
// https://isomorphic-git.org/docs/en/resolveRef
/** https://isomorphic-git.org/docs/en/resolveRef */
async resolveRef(options: { dir: string; ref: string }): Promise<string> {
const { dir, ref } = options;
return git.resolveRef({ fs, dir, ref });
}
/** https://isomorphic-git.org/docs/en/log */
async log(options: {
dir: string;
ref?: string;
}): Promise<ReadCommitResult[]> {
const { dir, ref } = options;
return git.log({
fs,
dir,
ref: ref ?? 'HEAD',
});
}
private onAuth = () => ({
username: this.config.username,
password: this.config.password,
@@ -228,6 +228,10 @@ export function applyCspDirectives(
const result: ContentSecurityPolicyOptions['directives'] =
helmet.contentSecurityPolicy.getDefaultDirectives();
// TODO(Rugvip): We currently use non-precompiled AJV for validation in the frontend, which uses eval.
// It should be replaced by any other solution that doesn't require unsafe-eval.
result['script-src'] = ["'self'", "'unsafe-eval'"];
if (directives) {
for (const [key, value] of Object.entries(directives)) {
if (value === false) {
+13
View File
@@ -1,5 +1,18 @@
# @backstage/backend-tasks
## 0.1.2
### Patch Changes
- e188b37024: Updated README to clarify the following:
- Dashes cannot be used in the `id`, changed to an underscore in the example
- The `timeout` is required, this was also added to the example
- Added a note about tasks not running as expected for local development using persistent database
- Updated dependencies
- @backstage/backend-common@0.10.2
## 0.1.1
### Patch Changes
+6 -1
View File
@@ -21,14 +21,19 @@ import { Duration } from 'luxon';
const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin');
await scheduler.scheduleTask({
id: 'refresh-things',
id: 'refresh_things',
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
fn: async () => {
await entityProvider.run();
},
});
```
## Local Development
When working with the `@backstage/backend-tasks` library you may run into your task not running immediately at startup as expected if you are using a persistent database. This is by design - the library respects the previous state and does not run the task sooner than the specified frequency. If you want to get around this, there is a table called `backstage_backend_tasks__tasks` in the applicable plugin's database which will contain a record with the next run date and time. You can delete this record to get things back to what you expect.
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
"version": "0.1.1",
"version": "0.1.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.0",
"@backstage/backend-common": "^0.10.2",
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.4",
"@backstage/types": "^0.1.1",
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.11",
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.5",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
+1 -1
View File
@@ -37,7 +37,7 @@
"mysql2": "^2.2.5",
"pg": "^8.3.0",
"sqlite3": "^5.0.1",
"testcontainers": "^7.23.0",
"testcontainers": "^8.1.2",
"uuid": "^8.0.0"
},
"devDependencies": {
+3 -3
View File
@@ -41,11 +41,11 @@ export default async function createPlugin({
discovery,
factRetrievers: [
createFactRetrieverRegistration(
'* * * * *', // Example cron, every minute
'1 1 1 * *', // Example cron, At 01:01 on day-of-month 1.
entityOwnershipFactRetriever,
),
createFactRetrieverRegistration('* * * * *', entityMetadataFactRetriever),
createFactRetrieverRegistration('* * * * *', techdocsFactRetriever),
createFactRetrieverRegistration('1 1 1 * *', entityMetadataFactRetriever),
createFactRetrieverRegistration('1 1 1 * *', techdocsFactRetriever),
],
factCheckerFactory: new JsonRulesEngineFactCheckerFactory({
checks: [
+8
View File
@@ -1,5 +1,13 @@
# @backstage/cli
## 0.10.5
### Patch Changes
- 37123b4851: Bump `css-loader` from `5.2.6` to `6.5.1`
- 9534391ae4: Add --pattern option to override matching glob patterns for backstage versioning
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
## 0.10.4
### Patch Changes
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.10.4",
"version": "0.10.5",
"private": false,
"publishConfig": {
"access": "public"
@@ -59,7 +59,7 @@
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
"commander": "^6.1.0",
"css-loader": "^5.2.6",
"css-loader": "^6.5.1",
"diff": "^5.0.0",
"esbuild": "^0.14.1",
"eslint": "^7.30.0",
@@ -83,6 +83,7 @@
"json-schema": "^0.4.0",
"jest-transform-yaml": "^0.1.1",
"lodash": "^4.17.21",
"minimatch": "3.0.4",
"mini-css-extract-plugin": "^2.4.2",
"node-libs-browser": "^2.2.1",
"ora": "^5.3.0",
@@ -114,12 +115,12 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.10.1",
"@backstage/backend-common": "^0.10.2",
"@backstage/config": "^0.1.11",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-app-api": "^0.3.0",
"@backstage/dev-utils": "^0.2.15",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@backstage/theme": "^0.2.14",
"@types/diff": "^5.0.0",
@@ -127,6 +128,7 @@
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^8.1.3",
"@types/minimatch": "^3.0.5",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/recursive-readdir": "^2.2.0",
+4
View File
@@ -214,6 +214,10 @@ export function registerCommands(program: CommanderStatic) {
program
.command('versions:bump')
.option(
'--pattern <glob>',
'Override glob for matching packages to upgrade',
)
.description('Bump Backstage packages to the latest versions')
.action(lazy(() => import('./versions/bump').then(m => m.default)));
+145 -4
View File
@@ -16,6 +16,7 @@
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { Command } from 'commander';
import { resolve as resolvePath } from 'path';
import { paths } from '../../lib/paths';
import { mapDependencies } from '../../lib/versioning';
@@ -36,6 +37,8 @@ const REGISTRY_VERSIONS: { [name: string]: string } = {
'@backstage/core': '1.0.6',
'@backstage/core-api': '1.0.7',
'@backstage/theme': '2.0.0',
'@backstage-extra/custom': '1.1.0',
'@backstage-extra/custom-two': '2.0.0',
};
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
@@ -83,7 +86,7 @@ describe('bump', () => {
it('should bump backstage dependencies', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
await mapDependencies(paths.targetDir);
await mapDependencies(paths.targetDir, '@backstage/*');
mockFs({
'/yarn.lock': lockfileMock,
@@ -123,9 +126,10 @@ describe('bump', () => {
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump();
await bump({ pattern: null } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/core-api',
@@ -178,9 +182,145 @@ describe('bump', () => {
});
});
it('should bump backstage dependencies and dependencies matching pattern glob', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
await mapDependencies(paths.targetDir, '@backstage/*');
const customLockfileMock = `${lockfileMock}
"@backstage-extra/custom@^1.1.0":
version "1.1.0"
"@backstage-extra/custom@^1.0.1":
version "1.0.1"
"@backstage-extra/custom-two@^1.0.0":
version "1.0.0"
`;
const customLockfileMockResult = `${HEADER}
"@backstage-extra/custom-two@^1.0.0":
version "1.0.0"
"@backstage-extra/custom@^1.1.0":
version "1.1.0"
"@backstage/core@^1.0.5":
version "1.0.6"
dependencies:
"@backstage/core-api" "^1.0.6"
"@backstage/theme@^1.0.0":
version "1.0.0"
`;
mockFs({
'/yarn.lock': customLockfileMock,
'/lerna.json': JSON.stringify({
packages: ['packages/*'],
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5',
'@backstage-extra/custom': '^1.0.1',
'@backstage-extra/custom-two': '^1.0.0',
},
}),
'/packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3',
'@backstage/theme': '^1.0.0',
'@backstage-extra/custom': '^1.1.0',
'@backstage-extra/custom-two': '^1.0.0',
},
}),
});
paths.targetDir = '/';
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: '@{backstage,backstage-extra}/*' } as any);
});
expect(logs.filter(Boolean)).toEqual([
'Using custom pattern glob @{backstage,backstage-extra}/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage-extra/custom-two',
'Checking for updates of @backstage-extra/custom',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage-extra/custom-two in a to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'bumping @backstage-extra/custom-two in b to ^2.0.0',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0',
' @backstage/theme : 1.0.0 ~> 2.0.0',
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(6);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/core',
);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/theme',
);
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
expect(lockfileContents).toEqual(customLockfileMockResult);
const packageA = await fs.readJson('/packages/a/package.json');
expect(packageA).toEqual({
name: 'a',
dependencies: {
'@backstage-extra/custom': '^1.0.1',
'@backstage-extra/custom-two': '^2.0.0',
'@backstage/core': '^1.0.5', // not bumped since new version is within range
},
});
const packageB = await fs.readJson('/packages/b/package.json');
expect(packageB).toEqual({
name: 'b',
dependencies: {
'@backstage-extra/custom': '^1.1.0',
'@backstage-extra/custom-two': '^2.0.0',
'@backstage/core': '^1.0.3', // not bumped
'@backstage/theme': '^2.0.0', // bumped since newer
},
});
});
it('should ignore not found packages', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
await mapDependencies(paths.targetDir);
await mapDependencies(paths.targetDir, '@backstage/*');
mockFs({
'/yarn.lock': lockfileMockResult,
'/lerna.json': JSON.stringify({
@@ -209,9 +349,10 @@ describe('bump', () => {
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump();
await bump({ pattern: null } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
+19 -5
View File
@@ -17,6 +17,8 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import semver from 'semver';
import minimatch from 'minimatch';
import { Command } from 'commander';
import { isError } from '@backstage/errors';
import { resolve as resolvePath } from 'path';
import { run } from '../../lib/run';
@@ -26,7 +28,7 @@ import {
fetchPackageInfo,
Lockfile,
} from '../../lib/versioning';
import { includedFilter, forbiddenDuplicatesFilter } from './lint';
import { forbiddenDuplicatesFilter } from './lint';
import { BACKSTAGE_JSON } from '@backstage/cli-common';
const DEP_TYPES = [
@@ -36,6 +38,8 @@ const DEP_TYPES = [
'optionalDependencies',
];
const DEFAULT_PATTERN_GLOB = '@backstage/*';
type PkgVersionInfo = {
range: string;
target: string;
@@ -43,14 +47,22 @@ type PkgVersionInfo = {
location: string;
};
export default async () => {
export default async (cmd: Command) => {
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
let pattern = cmd.pattern;
if (!pattern) {
console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`);
pattern = DEFAULT_PATTERN_GLOB;
} else {
console.log(`Using custom pattern glob ${pattern}`);
}
const findTargetVersion = createVersionFinder();
// First we discover all Backstage dependencies within our own repo
const dependencyMap = await mapDependencies(paths.targetDir);
const dependencyMap = await mapDependencies(paths.targetDir, pattern);
// Next check with the package registry to see which dependency ranges we need to bump
const versionBumps = new Map<string, PkgVersionInfo[]>();
@@ -88,10 +100,12 @@ export default async () => {
}
});
const filter = (name: string) => minimatch(name, pattern);
// Check for updates of transitive backstage dependencies
await workerThreads(16, lockfile.keys(), async name => {
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
if (!includedFilter(name)) {
if (!filter(name)) {
return;
}
@@ -237,7 +251,7 @@ export default async () => {
// Finally we make sure the new lockfile doesn't have any duplicates
const dedupLockfile = await Lockfile.load(lockfilePath);
const result = dedupLockfile.analyze({
filter: includedFilter,
filter,
});
if (result.newVersions.length > 0) {
@@ -82,7 +82,10 @@ describe('mapDependencies', () => {
}),
});
const dependencyMap = await mapDependencies(paths.targetDir);
const dependencyMap = await mapDependencies(
paths.targetDir,
'@backstage/*',
);
expect(Array.from(dependencyMap)).toEqual([
[
'@backstage/core',
+3 -4
View File
@@ -13,12 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import minimatch from 'minimatch';
import { runPlain } from '../../lib/run';
import { NotFoundError } from '../errors';
const PREFIX = '@backstage';
const DEP_TYPES = [
'dependencies',
'devDependencies',
@@ -66,6 +64,7 @@ export async function fetchPackageInfo(
/** Map all dependencies in the repo as dependency => dependents */
export async function mapDependencies(
targetDir: string,
pattern: string,
): Promise<Map<string, PkgVersionInfo[]>> {
const { Project } = require('@lerna/project');
const project = new Project(targetDir);
@@ -78,7 +77,7 @@ export async function mapDependencies(
);
for (const [name, range] of deps) {
if (name.startsWith(PREFIX)) {
if (minimatch(name, pattern)) {
dependencyMap.set(
name,
(dependencyMap.get(name) ?? []).concat({
@@ -2,7 +2,7 @@ import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import Alert from '@material-ui/lab/Alert';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
const useStyles = makeStyles({
avatar: {
+9
View File
@@ -1,5 +1,14 @@
# @backstage/codemods
## 0.1.28
### Patch Changes
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/core-app-api@0.3.1
- @backstage/core-components@0.8.3
## 0.1.27
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/codemods",
"description": "A collection of codemods for Backstage projects",
"version": "0.1.27",
"version": "0.1.28",
"private": false,
"publishConfig": {
"access": "public",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/core-app-api
## 0.3.1
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/core-components@0.8.3
## 0.3.0
### Minor Changes
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-app-api",
"description": "Core app API used by Backstage apps",
"version": "0.3.0",
"version": "0.3.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,9 +30,9 @@
},
"dependencies": {
"@backstage/app-defaults": "^0.1.3",
"@backstage/core-components": "^0.8.2",
"@backstage/core-components": "^0.8.3",
"@backstage/config": "^0.1.11",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/theme": "^0.2.14",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.1",
@@ -50,7 +50,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.5",
"@backstage/test-utils": "^0.2.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+1 -1
View File
@@ -24,7 +24,7 @@ import React, {
useState,
} from 'react';
import { Route, Routes } from 'react-router-dom';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import {
ApiProvider,
ApiRegistry,
@@ -16,7 +16,7 @@
import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react';
import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api';
import { useObservable } from 'react-use';
import useObservable from 'react-use/lib/useObservable';
// This tries to find the most accurate match, but also falls back to less
// accurate results in order to avoid errors.
+8
View File
@@ -1,5 +1,13 @@
# @backstage/core-components
## 0.8.3
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
## 0.8.2
### Patch Changes
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
"version": "0.8.2",
"version": "0.8.3",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.11",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-plugin-api": "^0.4.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.3.0",
"@backstage/cli": "^0.10.3",
"@backstage/core-app-api": "^0.3.1",
"@backstage/cli": "^0.10.5",
"@backstage/test-utils": "^0.2.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -19,7 +19,7 @@ import { act, fireEvent } from '@testing-library/react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { CopyTextButton } from './CopyTextButton';
import { errorApiRef } from '@backstage/core-plugin-api';
import { useCopyToClipboard } from 'react-use';
import useCopyToClipboard from 'react-use/lib/useCopyToClipboard';
jest.mock('popper.js', () => {
const PopperJS = jest.requireActual('popper.js');
@@ -32,14 +32,12 @@ jest.mock('popper.js', () => {
};
});
jest.mock('react-use', () => {
const original = jest.requireActual('react-use');
jest.mock('react-use/lib/useCopyToClipboard', () => {
const original = jest.requireActual('react-use/lib/useCopyToClipboard');
return {
...original,
useCopyToClipboard: jest
.fn()
.mockImplementation(original.useCopyToClipboard),
__esModule: true,
default: jest.fn().mockImplementation(original.default),
};
});
@@ -19,7 +19,7 @@ import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import CopyIcon from '@material-ui/icons/FileCopy';
import React, { MouseEventHandler, useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import useCopyToClipboard from 'react-use/lib/useCopyToClipboard';
/**
* Properties for {@link CopyTextButton}
@@ -16,7 +16,7 @@
import React, { ReactNode, useState, useEffect } from 'react';
import { useApi, storageApiRef } from '@backstage/core-plugin-api';
import { useObservable } from 'react-use';
import useObservable from 'react-use/lib/useObservable';
import classNames from 'classnames';
import { makeStyles } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
@@ -15,7 +15,7 @@
*/
import { useMemo, useState } from 'react';
import { useToggle } from 'react-use';
import useToggle from 'react-use/lib/useToggle';
import { AnsiLine } from './AnsiProcessor';
export function applySearchFilter(lines: AnsiLine[], searchText: string) {
@@ -16,7 +16,7 @@
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import useCopyToClipboard from 'react-use/lib/useCopyToClipboard';
import { AnsiLine } from './AnsiProcessor';
export function useLogViewerSelection(lines: AnsiLine[]) {
@@ -22,7 +22,7 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import List from '@material-ui/core/List';
import Button from '@material-ui/core/Button';
import React, { useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import useObservable from 'react-use/lib/useObservable';
import LoginRequestListItem from './LoginRequestListItem';
import { useApi, oauthRequestApiRef } from '@backstage/core-plugin-api';
@@ -18,7 +18,7 @@ import { makeStyles } from '@material-ui/core/styles';
import Tooltip, { TooltipProps } from '@material-ui/core/Tooltip';
import React, { useState } from 'react';
import TextTruncate, { TextTruncateProps } from 'react-text-truncate';
import { useMountedState } from 'react-use';
import useMountedState from 'react-use/lib/useMountedState';
type Props = {
text: TextTruncateProps['text'];
@@ -21,7 +21,7 @@ import { makeStyles } from '@material-ui/core/styles';
import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import { chunkArray } from './utils';
import { useWindowSize } from 'react-use';
import useWindowSize from 'react-use/lib/useWindowSize';
/* Import Components */
@@ -18,7 +18,7 @@ import { isEqual } from 'lodash';
import qs from 'qs';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useDebounce } from 'react-use';
import useDebounce from 'react-use/lib/useDebounce';
function stringify(queryParams: any): string {
// Even though these setting don't look nice (e.g. escaped brackets), we should keep
@@ -21,7 +21,7 @@ import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
import React, { useContext, useState } from 'react';
import { useLocalStorage } from 'react-use';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import {
sidebarConfig,
SidebarContext,
@@ -25,7 +25,7 @@ import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import React, { useState } from 'react';
import { useMount } from 'react-use';
import useMount from 'react-use/lib/useMount';
import { Progress } from '../../components/Progress';
import { Content } from '../Content/Content';
import { ContentHeader } from '../ContentHeader/ContentHeader';
+6
View File
@@ -1,5 +1,11 @@
# @backstage/core-plugin-api
## 0.4.1
### Patch Changes
- c534ef2242: Deprecated `OldIconComponent`. Existing usage should be replaced with `IconComponent`.
## 0.4.0
### Minor Changes
+1 -1
View File
@@ -623,7 +623,7 @@ export const oktaAuthApiRef: ApiRef<
SessionApi
>;
// @public
// @public @deprecated
export type OldIconComponent = ComponentType<SvgIconProps>;
// @alpha
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-plugin-api",
"description": "Core API used by Backstage plugins",
"version": "0.4.0",
"version": "0.4.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -45,8 +45,8 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/core-app-api": "^0.3.0",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/test-utils": "^0.2.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -43,5 +43,6 @@ export type IconComponent = ComponentType<{
* migration, but it is not exported.
*
* @public
* @deprecated This type will be removed, use {@link IconComponent} instead.
*/
export type OldIconComponent = ComponentType<SvgIconProps>;
+2
View File
@@ -1,5 +1,7 @@
# @backstage/create-app
## 0.4.11
## 0.4.10
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
"version": "0.4.10",
"version": "0.4.11",
"private": false,
"publishConfig": {
"access": "public"
+11
View File
@@ -1,5 +1,16 @@
# @backstage/dev-utils
## 0.2.16
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/plugin-catalog-react@0.6.10
- @backstage/core-app-api@0.3.1
- @backstage/core-components@0.8.3
## 0.2.15
### Patch Changes
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.2.15",
"version": "0.2.16",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,12 +30,12 @@
},
"dependencies": {
"@backstage/app-defaults": "^0.1.3",
"@backstage/core-app-api": "^0.3.0",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-app-api": "^0.3.1",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/catalog-model": "^0.9.7",
"@backstage/integration-react": "^0.1.16",
"@backstage/plugin-catalog-react": "^0.6.8",
"@backstage/plugin-catalog-react": "^0.6.10",
"@backstage/test-utils": "^0.2.0",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
@@ -55,7 +55,7 @@
"react-dom": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.5",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
@@ -18,7 +18,7 @@ import { appThemeApiRef, useApi } from '@backstage/core-plugin-api';
import { ListItemIcon, ListItemText, Menu, MenuItem } from '@material-ui/core';
import AutoIcon from '@material-ui/icons/BrightnessAuto';
import React, { cloneElement, useCallback, useState } from 'react';
import { useObservable } from 'react-use';
import useObservable from 'react-use/lib/useObservable';
type ThemeIconProps = {
active?: boolean;
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-allure
## 0.1.10
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/plugin-catalog-react@0.6.10
- @backstage/core-components@0.8.3
## 0.1.9
### Patch Changes
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-allure",
"description": "A Backstage plugin that integrates with Allure",
"version": "0.1.9",
"version": "0.1.10",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -23,9 +23,9 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.9.7",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/plugin-catalog-react": "^0.6.8",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/plugin-catalog-react": "^0.6.10",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -37,9 +37,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.4",
"@backstage/core-app-api": "^0.3.0",
"@backstage/dev-utils": "^0.2.15",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -26,7 +26,7 @@ import {
MissingAnnotationEmptyState,
Progress,
} from '@backstage/core-components';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import { Entity } from '@backstage/catalog-model';
const AllureReport = (props: { entity: Entity }) => {
+5 -5
View File
@@ -22,8 +22,8 @@
},
"dependencies": {
"@backstage/config": "^0.1.5",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -35,9 +35,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.4",
"@backstage/core-app-api": "^0.3.0",
"@backstage/dev-utils": "^0.2.15",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-apache-airflow
## 0.1.2
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/core-components@0.8.3
## 0.1.1
### Patch Changes
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
"version": "0.1.1",
"version": "0.1.2",
"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.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -33,9 +33,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.4",
"@backstage/core-app-api": "^0.3.0",
"@backstage/dev-utils": "^0.2.15",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -31,7 +31,7 @@ import Typography from '@material-ui/core/Typography';
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import { apacheAirflowApiRef } from '../../api';
import { Dag } from '../../api/types';
import { ScheduleIntervalLabel } from '../ScheduleIntervalLabel';
@@ -22,7 +22,7 @@ import {
import { useApi } from '@backstage/core-plugin-api';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import { apacheAirflowApiRef } from '../../api';
import { InstanceStatus } from '../../api/types';
@@ -22,7 +22,7 @@ import {
import { useApi } from '@backstage/core-plugin-api';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import { apacheAirflowApiRef } from '../../api';
import { InstanceVersion } from '../../api/types';
+11
View File
@@ -1,5 +1,16 @@
# @backstage/plugin-api-docs
## 0.6.21
### Patch Changes
- 11b81683a9: Support customizing index page layouts via outlets
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/plugin-catalog-react@0.6.10
- @backstage/core-components@0.8.3
- @backstage/plugin-catalog@0.7.7
## 0.6.20
### Patch Changes
+23 -19
View File
@@ -5,7 +5,6 @@
```ts
/// <reference types="react" />
import { Action } from '@material-table/core';
import { ApiEntity } from '@backstage/catalog-model';
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
@@ -15,6 +14,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { TableColumn } from '@backstage/core-components';
import { TableProps } from '@backstage/core-components';
import { UserListFilterKind } from '@backstage/plugin-catalog-react';
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
@@ -53,27 +53,17 @@ const apiDocsPlugin: BackstagePlugin<
export { apiDocsPlugin };
export { apiDocsPlugin as plugin };
// @public
export const ApiExplorerIndexPage: (
props: DefaultApiExplorerPageProps,
) => JSX.Element;
// Warning: (ae-missing-release-tag) "ApiExplorerPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ApiExplorerPage: ({
initiallySelectedFilter,
columns,
actions,
}: {
initiallySelectedFilter?: UserListFilterKind | undefined;
columns?: TableColumn<CatalogTableRow>[] | undefined;
actions?:
| (
| Action<CatalogTableRow>
| {
action: (rowData: CatalogTableRow) => Action<CatalogTableRow>;
position: string;
}
| ((rowData: CatalogTableRow) => Action<CatalogTableRow>)
)[]
| undefined;
}) => JSX.Element;
export const ApiExplorerPage: (
props: DefaultApiExplorerPageProps,
) => JSX.Element;
// Warning: (ae-missing-release-tag) "ApiTypeTitle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -110,6 +100,20 @@ export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element;
// @public (undocumented)
export const ConsumingComponentsCard: ({ variant }: Props_5) => JSX.Element;
// @public
export const DefaultApiExplorerPage: ({
initiallySelectedFilter,
columns,
actions,
}: DefaultApiExplorerPageProps) => JSX.Element;
// @public
export type DefaultApiExplorerPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['actions'];
};
// Warning: (ae-missing-release-tag) "defaultDefinitionWidgets" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+8 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-api-docs",
"description": "A Backstage plugin that helps represent API entities in the frontend",
"version": "0.6.20",
"version": "0.6.21",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,10 +32,10 @@
"dependencies": {
"@asyncapi/react-component": "^1.0.0-next.25",
"@backstage/catalog-model": "^0.9.7",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/plugin-catalog": "^0.7.6",
"@backstage/plugin-catalog-react": "^0.6.9",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/plugin-catalog": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.6.10",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -54,9 +54,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.4",
"@backstage/core-app-api": "^0.3.0",
"@backstage/dev-utils": "^0.2.15",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* 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.
@@ -14,185 +14,31 @@
* limitations under the License.
*/
import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/core-app-api';
import { TableColumn, TableProps } from '@backstage/core-components';
import {
ConfigApi,
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { CatalogTableRow } from '@backstage/plugin-catalog';
import {
CatalogApi,
catalogApiRef,
DefaultStarredEntitiesApi,
entityRouteRef,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import {
MockStorageApi,
TestApiProvider,
wrapInTestApp,
} from '@backstage/test-utils';
import DashboardIcon from '@material-ui/icons/Dashboard';
import { render } from '@testing-library/react';
import React from 'react';
import { apiDocsConfigRef } from '../../config';
import { renderInTestApp } from '@backstage/test-utils';
import { useOutlet } from 'react-router';
import { ApiExplorerPage } from './ApiExplorerPage';
describe('ApiCatalogPage', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'Entity1',
},
spec: { type: 'openapi' },
},
] as Entity[],
}),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
getEntityByName: async entityName => {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: entityName.name },
relations: [
{
type: RELATION_MEMBER_OF,
target: { namespace: 'default', kind: 'Group', name: 'tools' },
},
],
};
},
};
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useOutlet: jest.fn().mockReturnValue('Route Children'),
}));
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
},
jest.mock('./DefaultApiExplorerPage', () => ({
DefaultApiExplorerPage: jest.fn().mockReturnValue('DefaultApiExplorerPage'),
}));
describe('ApiExplorerPage', () => {
it('renders provided router element', async () => {
const { getByText } = await renderInTestApp(<ApiExplorerPage />);
expect(getByText('Route Children')).toBeInTheDocument();
});
const apiDocsConfig = {
getApiDefinitionWidget: () => undefined,
};
it('renders DefaultApiExplorerPage home when no router children are provided', async () => {
(useOutlet as jest.Mock).mockReturnValueOnce(null);
const { getByText } = await renderInTestApp(<ApiExplorerPage />);
const storageApi = MockStorageApi.create();
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[configApiRef, configApi],
[storageApiRef, storageApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
),
);
// this test right now causes some red lines in the log output when running tests
// related to some theme issues in mui-table
// https://github.com/mbrn/material-table/issues/1293
it('should render', async () => {
const { findByText } = renderWrapped(<ApiExplorerPage />);
expect(await findByText(/My Company API Explorer/)).toBeInTheDocument();
});
it('should render the default column of the grid', async () => {
const { getAllByRole } = renderWrapped(<ApiExplorerPage />);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual([
'Name',
'System',
'Owner',
'Type',
'Lifecycle',
'Description',
'Tags',
'Actions',
]);
});
it('should render the custom column passed as prop', async () => {
const columns: TableColumn<CatalogTableRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = renderWrapped(
<ApiExplorerPage columns={columns} />,
);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']);
});
it('should render the default actions of an item in the grid', async () => {
const { findByTitle, findByText } = await renderWrapped(
<ApiExplorerPage />,
);
expect(await findByText(/All \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/View/)).toBeInTheDocument();
expect(await findByTitle(/View/)).toBeInTheDocument();
expect(await findByTitle(/Edit/)).toBeInTheDocument();
expect(await findByTitle(/Add to favorites/)).toBeInTheDocument();
});
it('should render the custom actions of an item passed as prop', async () => {
const actions: TableProps<CatalogTableRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { findByTitle, findByText } = await renderWrapped(
<ApiExplorerPage actions={actions} />,
);
expect(await findByText(/All \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/Foo Action/)).toBeInTheDocument();
expect(await findByTitle(/Bar Action/)).toBeInTheDocument();
expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled();
expect(getByText('DefaultApiExplorerPage')).toBeInTheDocument();
});
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* 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.
@@ -14,97 +14,19 @@
* limitations under the License.
*/
import {
Content,
ContentHeader,
CreateButton,
PageWithHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
CatalogTable,
CatalogTableRow,
FilteredEntityLayout,
EntityListContainer,
FilterContainer,
} from '@backstage/plugin-catalog';
import {
EntityKindPicker,
EntityLifecyclePicker,
EntityListProvider,
EntityOwnerPicker,
EntityTagPicker,
EntityTypePicker,
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { createComponentRouteRef } from '../../routes';
import { useOutlet } from 'react-router';
import {
DefaultApiExplorerPage,
DefaultApiExplorerPageProps,
} from './DefaultApiExplorerPage';
const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createSystemColumn(),
CatalogTable.columns.createOwnerColumn(),
CatalogTable.columns.createSpecTypeColumn(),
CatalogTable.columns.createSpecLifecycleColumn(),
CatalogTable.columns.createMetadataDescriptionColumn(),
CatalogTable.columns.createTagsColumn(),
];
/**
* ApiExplorerPage
* @public
*/
export const ApiExplorerPage = (props: DefaultApiExplorerPageProps) => {
const outlet = useOutlet();
type ApiExplorerPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['actions'];
};
export const ApiExplorerPage = ({
initiallySelectedFilter = 'all',
columns,
actions,
}: ApiExplorerPageProps) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `${
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
const createComponentLink = useRouteRef(createComponentRouteRef);
return (
<PageWithHeader
themeId="apis"
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
>
<Content>
<ContentHeader title="">
<CreateButton
title="Register Existing API"
to={createComponentLink?.()}
/>
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable
columns={columns || defaultColumns}
actions={actions}
/>
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
return outlet || <DefaultApiExplorerPage {...props} />;
};
@@ -0,0 +1,198 @@
/*
* 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 { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/core-app-api';
import { TableColumn, TableProps } from '@backstage/core-components';
import {
ConfigApi,
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { CatalogTableRow } from '@backstage/plugin-catalog';
import {
CatalogApi,
catalogApiRef,
DefaultStarredEntitiesApi,
entityRouteRef,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import {
MockStorageApi,
TestApiProvider,
wrapInTestApp,
} from '@backstage/test-utils';
import DashboardIcon from '@material-ui/icons/Dashboard';
import { render } from '@testing-library/react';
import React from 'react';
import { apiDocsConfigRef } from '../../config';
import { DefaultApiExplorerPage } from './DefaultApiExplorerPage';
describe('DefaultApiExplorerPage', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'Entity1',
},
spec: { type: 'openapi' },
},
] as Entity[],
}),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
getEntityByName: async entityName => {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: entityName.name },
relations: [
{
type: RELATION_MEMBER_OF,
target: { namespace: 'default', kind: 'Group', name: 'tools' },
},
],
};
},
};
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
},
});
const apiDocsConfig = {
getApiDefinitionWidget: () => undefined,
};
const storageApi = MockStorageApi.create();
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[configApiRef, configApi],
[storageApiRef, storageApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
),
);
// this test right now causes some red lines in the log output when running tests
// related to some theme issues in mui-table
// https://github.com/mbrn/material-table/issues/1293
it('should render', async () => {
const { findByText } = renderWrapped(<DefaultApiExplorerPage />);
expect(await findByText(/My Company API Explorer/)).toBeInTheDocument();
});
it('should render the default column of the grid', async () => {
const { getAllByRole } = renderWrapped(<DefaultApiExplorerPage />);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual([
'Name',
'System',
'Owner',
'Type',
'Lifecycle',
'Description',
'Tags',
'Actions',
]);
});
it('should render the custom column passed as prop', async () => {
const columns: TableColumn<CatalogTableRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = renderWrapped(
<DefaultApiExplorerPage columns={columns} />,
);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']);
});
it('should render the default actions of an item in the grid', async () => {
const { findByTitle, findByText } = await renderWrapped(
<DefaultApiExplorerPage />,
);
expect(await findByText(/All \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/View/)).toBeInTheDocument();
expect(await findByTitle(/View/)).toBeInTheDocument();
expect(await findByTitle(/Edit/)).toBeInTheDocument();
expect(await findByTitle(/Add to favorites/)).toBeInTheDocument();
});
it('should render the custom actions of an item passed as prop', async () => {
const actions: TableProps<CatalogTableRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { findByTitle, findByText } = await renderWrapped(
<DefaultApiExplorerPage actions={actions} />,
);
expect(await findByText(/All \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/Foo Action/)).toBeInTheDocument();
expect(await findByTitle(/Bar Action/)).toBeInTheDocument();
expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled();
});
});
@@ -0,0 +1,118 @@
/*
* 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 {
Content,
ContentHeader,
CreateButton,
PageWithHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
CatalogTable,
CatalogTableRow,
FilteredEntityLayout,
EntityListContainer,
FilterContainer,
} from '@backstage/plugin-catalog';
import {
EntityKindPicker,
EntityLifecyclePicker,
EntityListProvider,
EntityOwnerPicker,
EntityTagPicker,
EntityTypePicker,
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { createComponentRouteRef } from '../../routes';
const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createSystemColumn(),
CatalogTable.columns.createOwnerColumn(),
CatalogTable.columns.createSpecTypeColumn(),
CatalogTable.columns.createSpecLifecycleColumn(),
CatalogTable.columns.createMetadataDescriptionColumn(),
CatalogTable.columns.createTagsColumn(),
];
/**
* DefaultApiExplorerPageProps
* @public
*/
export type DefaultApiExplorerPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['actions'];
};
/**
* DefaultApiExplorerPage
* @public
*/
export const DefaultApiExplorerPage = ({
initiallySelectedFilter = 'all',
columns,
actions,
}: DefaultApiExplorerPageProps) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `${
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
const createComponentLink = useRouteRef(createComponentRouteRef);
return (
<PageWithHeader
themeId="apis"
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
>
<Content>
<ContentHeader title="">
<CreateButton
title="Register Existing API"
to={createComponentLink?.()}
/>
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable
columns={columns || defaultColumns}
actions={actions}
/>
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
};
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* 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.
@@ -14,4 +14,6 @@
* limitations under the License.
*/
export { ApiExplorerPage } from './ApiExplorerPage';
export { ApiExplorerPage as ApiExplorerIndexPage } from './ApiExplorerPage';
export { DefaultApiExplorerPage } from './DefaultApiExplorerPage';
export type { DefaultApiExplorerPageProps } from './DefaultApiExplorerPage';
+1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './ApiExplorerPage';
export * from './ApiDefinitionCard';
export * from './ApisCards';
export * from './AsyncApiDefinitionWidget';
+1 -1
View File
@@ -53,7 +53,7 @@ export const ApiExplorerPage = apiDocsPlugin.provide(
createRoutableExtension({
name: 'ApiExplorerPage',
component: () =>
import('./components/ApiExplorerPage').then(m => m.ApiExplorerPage),
import('./components/ApiExplorerPage').then(m => m.ApiExplorerIndexPage),
mountPoint: rootRoute,
}),
);
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-auth-backend
## 0.6.1
### Patch Changes
- e0e57817d2: Added Google Cloud Identity-Aware Proxy as an identity provider.
- Updated dependencies
- @backstage/backend-common@0.10.2
## 0.6.0
### Minor Changes
+32 -6
View File
@@ -9,6 +9,7 @@ import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { JsonValue } from '@backstage/types';
import { JSONWebKey } from 'jose';
import { Logger as Logger_2 } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
@@ -59,8 +60,8 @@ export type Auth0ProviderOptions = {
};
// @public
export type AuthHandler<AuthResult> = (
input: AuthResult,
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
) => Promise<AuthHandlerResult>;
// @public
@@ -249,6 +250,11 @@ export const createBitbucketProvider: (
options?: BitbucketProviderOptions | undefined,
) => AuthProviderFactory;
// @public
export function createGcpIapProvider(
options: GcpIapProviderOptions,
): AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -335,6 +341,26 @@ export const encodeState: (state: OAuthState) => string;
// @public (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
// @public
export type GcpIapProviderOptions = {
authHandler?: AuthHandler<GcpIapResult>;
signIn: {
resolver: SignInResolver<GcpIapResult>;
};
};
// @public
export type GcpIapResult = {
iapToken: GcpIapTokenInfo;
};
// @public
export type GcpIapTokenInfo = {
sub: string;
email: string;
[key: string]: JsonValue;
};
// Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -661,14 +687,14 @@ export type SamlProviderOptions = {
};
// @public
export type SignInInfo<AuthResult> = {
export type SignInInfo<TAuthResult> = {
profile: ProfileInfo;
result: AuthResult;
result: TAuthResult;
};
// @public
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
+7 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
"version": "0.6.0",
"version": "0.6.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,12 +30,13 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.1",
"@backstage/backend-common": "^0.10.2",
"@backstage/catalog-client": "^0.5.3",
"@backstage/catalog-model": "^0.9.8",
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.5",
"@backstage/test-utils": "^0.2.1",
"@backstage/types": "^0.1.1",
"@google-cloud/firestore": "^4.15.1",
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
@@ -46,6 +47,7 @@
"express-promise-router": "^4.1.0",
"express-session": "^1.17.1",
"fs-extra": "9.1.0",
"google-auth-library": "^7.6.1",
"helmet": "^4.0.0",
"jose": "^1.27.1",
"jwt-decode": "^3.1.0",
@@ -72,7 +74,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.4",
"@backstage/cli": "^0.10.5",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/express-session": "^1.17.2",
@@ -83,7 +85,8 @@
"@types/passport-saml": "^1.1.3",
"@types/passport-strategy": "^0.2.35",
"@types/xml2js": "^0.4.7",
"msw": "^0.35.0"
"msw": "^0.35.0",
"supertest": "^6.1.3"
},
"files": [
"dist",
@@ -13,5 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createAwsAlbProvider } from './provider';
export type { AwsAlbProviderOptions } from './provider';
@@ -18,7 +18,7 @@ import express from 'express';
import { JWT } from 'jose';
import {
ALB_ACCESSTOKEN_HEADER,
ALB_ACCESS_TOKEN_HEADER,
ALB_JWT_HEADER,
AwsAlbAuthProvider,
} from './provider';
@@ -80,7 +80,7 @@ describe('AwsAlbAuthProvider', () => {
header: jest.fn(name => {
if (name === ALB_JWT_HEADER) {
return mockJwt;
} else if (name === ALB_ACCESSTOKEN_HEADER) {
} else if (name === ALB_ACCESS_TOKEN_HEADER) {
return mockAccessToken;
}
return undefined;
@@ -88,7 +88,7 @@ describe('AwsAlbAuthProvider', () => {
} as unknown as express.Request;
const mockRequestWithoutJwt = {
header: jest.fn(name => {
if (name === ALB_ACCESSTOKEN_HEADER) {
if (name === ALB_ACCESS_TOKEN_HEADER) {
return mockAccessToken;
}
return undefined;
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AuthHandler,
AuthProviderFactory,
@@ -35,7 +36,7 @@ import { AuthenticationError } from '@backstage/errors';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken';
export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken';
type Options = {
region: string;
@@ -134,7 +135,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
private async getResult(req: express.Request): Promise<AwsAlbResult> {
const jwt = req.header(ALB_JWT_HEADER);
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER);
if (jwt === undefined) {
throw new AuthenticationError(
@@ -144,7 +145,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
if (accessToken === undefined) {
throw new AuthenticationError(
`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`,
`Missing ALB OIDC header: ${ALB_ACCESS_TOKEN_HEADER}`,
);
}
@@ -0,0 +1,134 @@
/*
* 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 { ConflictError } from '@backstage/errors';
import { OAuth2Client } from 'google-auth-library';
import { createTokenValidator, parseRequestToken } from './helpers';
const validJwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
beforeEach(() => {
jest.clearAllMocks();
});
describe('helpers', () => {
describe('createTokenValidator', () => {
it('runs the happy path', async () => {
const mockClient = {
getIapPublicKeys: async () => ({ pubkeys: '' }),
verifySignedJwtWithCertsAsync: async () => ({
getPayload: () => ({ sub: 's', email: 'e@mail.com' }),
}),
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).resolves.toMatchObject({
sub: 's',
email: 'e@mail.com',
});
});
it('throws if the client throws', async () => {
const mockClient = {
getIapPublicKeys: async () => {
throw new TypeError('bam');
},
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).rejects.toThrowError(TypeError);
});
it('rejects empty payload', async () => {
const mockClient = {
getIapPublicKeys: async () => ({ pubkeys: '' }),
verifySignedJwtWithCertsAsync: async () => ({
getPayload: () => undefined,
}),
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).rejects.toMatchObject({
name: 'TypeError',
message: 'Token had no payload',
});
});
});
describe('parseRequestToken', () => {
it('runs the happy path', async () => {
await expect(
parseRequestToken(
validJwt,
async () => ({ sub: 's', email: 'e@mail.com' } as any),
),
).resolves.toMatchObject({
iapToken: {
sub: 's',
email: 'e@mail.com',
},
});
});
it('rejects bad tokens', async () => {
await expect(
parseRequestToken(7, undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header: x-goog-iap-jwt-assertion',
});
await expect(
parseRequestToken(undefined, undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header: x-goog-iap-jwt-assertion',
});
await expect(
parseRequestToken('', undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header: x-goog-iap-jwt-assertion',
});
});
it('translates validator errors', async () => {
await expect(
parseRequestToken(validJwt, async () => {
throw new ConflictError('Ouch');
}),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Google IAP token verification failed, ConflictError: Ouch',
});
});
it('rejects bad token payloads', async () => {
await expect(
parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Google IAP token payload is missing sub and/or email claim',
});
});
});
});
@@ -0,0 +1,84 @@
/*
* 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 { AuthenticationError } from '@backstage/errors';
import { OAuth2Client, TokenPayload } from 'google-auth-library';
import { AuthHandler } from '../types';
import { GcpIapResult, IAP_JWT_HEADER } from './types';
export function createTokenValidator(
audience: string,
mockClient?: OAuth2Client,
): (token: string) => Promise<TokenPayload> {
const client = mockClient ?? new OAuth2Client();
return async function tokenValidator(token) {
// TODO(freben): Rate limit the public key reads. It may be sensible to
// cache these for some reasonable time rather than asking for the public
// keys on every single sign-in. But since the rate of events here is so
// slow, I decided to keep it simple for now.
const response = await client.getIapPublicKeys();
const ticket = await client.verifySignedJwtWithCertsAsync(
token,
response.pubkeys,
audience,
['https://cloud.google.com/iap'],
);
const payload = ticket.getPayload();
if (!payload) {
throw new TypeError('Token had no payload');
}
return payload;
};
}
export async function parseRequestToken(
jwtToken: unknown,
tokenValidator: (token: string) => Promise<TokenPayload>,
): Promise<GcpIapResult> {
if (typeof jwtToken !== 'string' || !jwtToken) {
throw new AuthenticationError(
`Missing Google IAP header: ${IAP_JWT_HEADER}`,
);
}
let payload: TokenPayload;
try {
payload = await tokenValidator(jwtToken);
} catch (e) {
throw new AuthenticationError(`Google IAP token verification failed, ${e}`);
}
if (!payload.sub || !payload.email) {
throw new AuthenticationError(
'Google IAP token payload is missing sub and/or email claim',
);
}
return {
iapToken: {
...payload,
sub: payload.sub,
email: payload.email,
},
};
}
export const defaultAuthHandler: AuthHandler<GcpIapResult> = async ({
iapToken,
}) => ({ profile: { email: iapToken.email } });
@@ -0,0 +1,22 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createGcpIapProvider } from './provider';
export type {
GcpIapProviderOptions,
GcpIapResult,
GcpIapTokenInfo,
} from './types';
@@ -0,0 +1,76 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { GcpIapProvider } from './provider';
beforeEach(() => {
jest.clearAllMocks();
});
describe('GcpIapProvider', () => {
const authHandler = jest.fn();
const signInResolver = jest.fn();
const tokenValidator = jest.fn();
const logger = getVoidLogger();
it('runs the happy path', async () => {
const provider = new GcpIapProvider({
authHandler,
signInResolver,
tokenValidator,
tokenIssuer: {} as any,
catalogIdentityClient: {} as any,
logger,
});
// { "sub": "user:default/me", "ent": ["group:default/home"] }
const backstageToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc';
const iapToken = { sub: 's', email: 'e@mail.com' };
authHandler.mockResolvedValueOnce({ email: 'e@mail.com' });
signInResolver.mockResolvedValueOnce({ id: 'i', token: backstageToken });
tokenValidator.mockResolvedValueOnce(iapToken);
const app = express();
app.use('/refresh', provider.refresh.bind(provider));
const response = await request(app)
.get('/refresh')
.set('x-goog-iap-jwt-assertion', 'token');
expect(response.status).toBe(200);
expect(response.get('content-type')).toBe(
'application/json; charset=utf-8',
);
expect(response.body).toEqual({
backstageIdentity: {
id: 'i',
idToken: backstageToken,
token: backstageToken,
identity: {
type: 'user',
userEntityRef: 'user:default/me',
ownershipEntityRefs: ['group:default/home'],
},
},
providerInfo: { iapToken },
});
});
});
@@ -0,0 +1,125 @@
/*
* Copyright 2021 Spotify AB
*
* 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 express from 'express';
import { TokenPayload } from 'google-auth-library';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
import {
AuthHandler,
AuthProviderFactory,
AuthProviderRouteHandlers,
SignInResolver,
} from '../types';
import {
createTokenValidator,
defaultAuthHandler,
parseRequestToken,
} from './helpers';
import {
GcpIapProviderOptions,
GcpIapResponse,
GcpIapResult,
IAP_JWT_HEADER,
} from './types';
export class GcpIapProvider implements AuthProviderRouteHandlers {
private readonly authHandler: AuthHandler<GcpIapResult>;
private readonly signInResolver: SignInResolver<GcpIapResult>;
private readonly tokenValidator: (token: string) => Promise<TokenPayload>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: {
authHandler: AuthHandler<GcpIapResult>;
signInResolver: SignInResolver<GcpIapResult>;
tokenValidator: (token: string) => Promise<TokenPayload>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
}) {
this.authHandler = options.authHandler;
this.signInResolver = options.signInResolver;
this.tokenValidator = options.tokenValidator;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
}
async start() {}
async frameHandler() {}
async refresh(req: express.Request, res: express.Response): Promise<void> {
const result = await parseRequestToken(
req.header(IAP_JWT_HEADER),
this.tokenValidator,
);
const { profile } = await this.authHandler(result);
const backstageIdentity = await this.signInResolver(
{ profile, result },
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
const response: GcpIapResponse = {
providerInfo: { iapToken: result.iapToken },
profile,
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
};
res.json(response);
}
}
/**
* Creates an auth provider for Google Identity-Aware Proxy.
*
* @public
*/
export function createGcpIapProvider(
options: GcpIapProviderOptions,
): AuthProviderFactory {
return ({ config, tokenIssuer, catalogApi, logger }) => {
const audience = config.getString('audience');
const authHandler = options.authHandler ?? defaultAuthHandler;
const signInResolver = options.signIn.resolver;
const tokenValidator = createTokenValidator(audience);
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
return new GcpIapProvider({
authHandler,
signInResolver,
tokenValidator,
tokenIssuer,
catalogIdentityClient,
logger,
});
};
}
@@ -0,0 +1,96 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { JsonValue } from '@backstage/types';
import { AuthHandler, AuthResponse, SignInResolver } from '../types';
/**
* The header name used by the IAP.
*/
export const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
/**
* The data extracted from an IAP token.
*
* @public
*/
export type GcpIapTokenInfo = {
/**
* The unique, stable identifier for the user.
*/
sub: string;
/**
* User email address.
*/
email: string;
/**
* Other fields.
*/
[key: string]: JsonValue;
};
/**
* The result of the initial auth challenge. This is the input to the auth
* callbacks.
*
* @public
*/
export type GcpIapResult = {
/**
* The data extracted from the IAP token header.
*/
iapToken: GcpIapTokenInfo;
};
/**
* The provider info to return to the frontend.
*/
export type GcpIapProviderInfo = {
/**
* The data extracted from the IAP token header.
*/
iapToken: GcpIapTokenInfo;
};
/**
* The shape of the response to return to callers.
*/
export type GcpIapResponse = AuthResponse<GcpIapProviderInfo>;
/**
* Options for {@link createGcpIapProvider}.
*
* @public
*/
export type GcpIapProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth
* response into the profile that will be presented to the user. The default
* implementation just provides the authenticated email that the IAP
* presented.
*/
authHandler?: AuthHandler<GcpIapResult>;
/**
* Configures sign-in for this provider.
*/
signIn: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<GcpIapResult>;
};
};
@@ -27,6 +27,7 @@ export * from './oidc';
export * from './okta';
export * from './onelogin';
export * from './saml';
export * from './gcp-iap';
export { factories as defaultAuthProviderFactories } from './factories';
@@ -22,7 +22,9 @@ function parseJwtPayload(token: string) {
}
/**
* Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token
* Parses a Backstage-issued token and decorates the
* {@link BackstageIdentityResponse} with identity information sourced from the
* token.
*
* @public
*/
+31 -18
View File
@@ -200,13 +200,16 @@ export interface BackstageSignInResult {
/**
* The old exported symbol for {@link BackstageSignInResult}.
*
* @public
* @deprecated Use the `BackstageSignInResult` type instead.
* @deprecated Use the {@link BackstageSignInResult} instead.
*/
export type BackstageIdentity = BackstageSignInResult;
/**
* Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider.
* Response object containing the {@link BackstageUserIdentity} and the token
* from the authentication provider.
*
* @public
*/
export interface BackstageIdentityResponse extends BackstageSignInResult {
@@ -220,7 +223,8 @@ export interface BackstageIdentityResponse extends BackstageSignInResult {
* Used to display login information to user, i.e. sidebar popup.
*
* It is also temporarily used as the profile of the signed-in user's Backstage
* identity, but we want to replace that with data from identity and/org catalog service
* identity, but we want to replace that with data from identity and/org catalog
* service
*
* @public
*/
@@ -241,28 +245,32 @@ export type ProfileInfo = {
};
/**
* type of sign in information context, includes the profile information and authentication result which contains auth. related information
* Type of sign in information context. Includes the profile information and
* authentication result which contains auth related information.
*
* @public
*/
export type SignInInfo<AuthResult> = {
export type SignInInfo<TAuthResult> = {
/**
* The simple profile passed down for use in the frontend.
*/
profile: ProfileInfo;
/**
* The authentication result that was received from the authentication provider.
* The authentication result that was received from the authentication
* provider.
*/
result: AuthResult;
result: TAuthResult;
};
/**
* Sign in resolver type describes the function which handles the result of a successful authentication
* and it must return a valid {@link BackstageSignInResult}
* Describes the function which handles the result of a successful
* authentication. Must return a valid {@link BackstageSignInResult}.
*
* @public
*/
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
@@ -271,23 +279,28 @@ export type SignInResolver<AuthResult> = (
) => Promise<BackstageSignInResult>;
/**
* The return type of authentication handler which must contain a valid profile information
* The return type of an authentication handler. Must contain valid profile
* information.
*
* @public
*/
export type AuthHandlerResult = { profile: ProfileInfo };
/**
* The AuthHandler function is called every time the user authenticates using the provider.
* The AuthHandler function is called every time the user authenticates using
* the provider.
*
* The handler should return a profile that represents the session for the user in the frontend.
* The handler should return a profile that represents the session for the user
* in the frontend.
*
* 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.
* 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,
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
) => Promise<AuthHandlerResult>;
export type StateEncoder = (

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