Merge remote-tracking branch 'upstream/master' into airbrake-initial
# Conflicts: # packages/app/package.json
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Bump `testcontainers` dependency to version `8.1.2`
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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! 🎅
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://www.cncf.io/projects)
|
||||
[](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 |
@@ -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
|
||||
|
||||
@@ -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}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -164,12 +164,110 @@ https://circleci.com.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)));
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
+1
-1
@@ -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: {
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -623,7 +623,7 @@ export const oktaAuthApiRef: ApiRef<
|
||||
SessionApi
|
||||
>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type OldIconComponent = ComponentType<SvgIconProps>;
|
||||
|
||||
// @alpha
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @backstage/create-app
|
||||
|
||||
## 0.4.11
|
||||
|
||||
## 0.4.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './ApiExplorerPage';
|
||||
export * from './ApiDefinitionCard';
|
||||
export * from './ApisCards';
|
||||
export * from './AsyncApiDefinitionWidget';
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user