Merge branch 'backstage:master' into topic/add-entity-pull-requests

This commit is contained in:
Andre Wanlin
2021-11-12 07:38:47 -06:00
committed by GitHub
314 changed files with 5625 additions and 1864 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Switched to dynamically determining the packages that are unsafe to repack when executing the CLI within the Backstage main repo.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Add theme switcher to sidebar of dev app.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
---
fix: kubernetes plugin shall pass id token on get clusters request if possible
-8
View File
@@ -1,8 +0,0 @@
---
'@backstage/core-components': patch
'@backstage/theme': patch
---
Added a warning variant to `DismissableBanner` component. If you are using a
custom theme, you will need to add the optional `palette.banner.warning` color,
otherwise this variant will fall back to the `palette.banner.error` color.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Update to the right version of @backstage/errors
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Tweak logic for msgraph catalog ingesting for display names with security groups
Previously security groups that weren't mail enabled were imported with UUIDs, now they use the display name.
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/plugin-gcp-projects': patch
---
UI updates to GCP-projects plugin
Adds the following to the project list page:
- pagination
- filtering
- sorting
- rows per page
- show/hide columns
Makes breadcrumb a link back to project list for the project details and new project views.
In project list page, updates New project button to use RouterLink instead of `href` to avoid login prompt.
In project details view, links to project details and logs now work, clicking on these will open the project or logs in GCP in new tab.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/backend-common': patch
---
AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-jenkins-backend': patch
---
Don't require a validation pattern for the Jenkins base URL.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
remove double config dep
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-components': patch
---
Allow for `cellStyle` property on `TableColumn` to be a function as well as `React.CSSProperties` as per the Material UI Table component
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Migrated to using `@backstage/app-defaults`.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/backend-common': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
---
Reading app config from a remote server
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Added the `isDatabaseConflictError` function.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/create-app': patch
---
Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future.
To migrate an existing application, add the latest version of `@backstage/app-defaults` as a dependency in `packages/app/package.json`, and make the following change to `packages/app/src/App.tsx`:
```diff
-import { createApp, FlatRoutes } from '@backstage/core-app-api';
+import { createApp } from '@backstage/app-defaults';
+import { FlatRoutes } from '@backstage/core-app-api';
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Pinning version of elastic search client to 7.13.0 to prevent breaking change towards third party ElasticSearch clusters on 7.14.0.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-ilert': patch
---
Change the version of `@date-io/luxon` from 2.x to 1.x to make it compatible with material-ui-pickers
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kafka': patch
---
Use IdentityApi to provide Auth Token for KafkaBackendClient Api calls
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/test-utils': patch
---
Update Keyboard deprecation with a link to the recommended successor
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Expose some classes and interfaces public so TaskWorkers can run externally from the scaffolder API.
-9
View File
@@ -1,9 +0,0 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-devops-backend': minor
'@backstage/plugin-azure-devops-common': patch
---
refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`.
Stop re-exporting types from `@backstage/plugin-azure-devops-backend`.
Added new types to `@backstage/plugin-azure-devops-common`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/catalog-client': patch
---
Improved API documentation for catalog-client.
-15
View File
@@ -1,15 +0,0 @@
---
'@backstage/techdocs-common': patch
---
1. Techdocs publisher constructors now use parameter objects when being
instantiated
2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for
creation to be aligned with other publishers; this does not impact
`LocalPublish` usage.
```diff
- const publisher = new LocalPublish(config, logger, discovery);
+ const publisher = LocalPublish.fromConfig(config, logger, discovery);
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
Only use settings that have a value when creating a new FirestoreKeyStore instance
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': minor
---
Tweaked style insertion logic to make sure that JSS stylesheets always receive the highest priority.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-sentry': patch
---
fix: sentry-plugin can forward identity token to backend (for case when it requires authorization)
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-code-coverage': patch
---
Change represented test date from epoch to something more human friendly. Round test coverage to 2 decimal places.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Make `ExitCodeError` call `super` early to avoid compiler warnings
-14
View File
@@ -1,14 +0,0 @@
---
'@backstage/create-app': patch
---
Updated the app template to no longer include the `--no-private` flag for the `create-plugin` command.
To apply this change to an existing application, remove the `--no-private` flag from the `create-plugin` command in the root `package.json`:
```diff
"prettier:check": "prettier --check .",
- "create-plugin": "backstage-cli create-plugin --scope internal --no-private",
+ "create-plugin": "backstage-cli create-plugin --scope internal",
"remove-plugin": "backstage-cli remove-plugin"
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/config-loader': patch
---
Bump msw to the same version as the rest
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-code-coverage': patch
---
Make dates in X-Axis sort in ascending order
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-components': patch
---
fixed route resolving (issue #7741) when user cannot select a tab in any of the tabbed pages (like the Catalog page) if it shares the same initial letters as a preceding tab. (i.e. where tab with a path of /ci is followed by a path of /ci-2, user cannot select /ci-2 as /ci will always be selected first).
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/backend-common': patch
---
Make sure that server builder `start()` propagates errors (such as failing to bind to the required port) properly and doesn't resolve the promise prematurely.
After this change, the backend logger will be able to actually capture the error as it happens:
```
2021-11-11T10:54:21.334Z backstage info Initializing http server
2021-11-11T10:54:21.335Z backstage error listen EADDRINUSE: address already in use :::7000 code=EADDRINUSE errno=-48 syscall=listen address=:: port=7000
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
Added forwarding of the `audience` option for the SAML provider, making it possible to enable `audience` verification.
-42
View File
@@ -1,42 +0,0 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events.
This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers.
It is intended to switch the endpoint globally for the whole instance.
If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`:
```tsx
// packages/app/src/apis.ts
// ...
import {
scaffolderApiRef,
ScaffolderClient,
} from '@backstage/plugin-scaffolder';
export const apis: AnyApiFactory[] = [
// ...
createApiFactory({
api: scaffolderApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
},
factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) =>
new ScaffolderClient({
discoveryApi,
identityApi,
scmIntegrationsApi,
// use long polling instead of an eventsource
useLongPollingLogs: true,
}),
}),
];
```
-14
View File
@@ -1,14 +0,0 @@
---
'@backstage/create-app': patch
---
Removed the version pinning of the packages `graphql-language-service-interface` and `graphql-language-service-parser`. This should no longer be necessary.
You can apply the same change in your repository by ensuring that the following does _NOT_ appear in your root `package.json`.
```json
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Removed the unused `UserFlags` type.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': patch
---
Deprecated the `theme` property on `AppTheme`, replacing it with `Provider`. See https://backstage.io/docs/api/deprecations#app-theme for more details.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Allow singleton and flexibly nested EntityFilters
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': patch
---
Migrated to using `createSpecializedApp`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-react': patch
---
export `loadIdentityOwnerRefs` and `loadCatalogOwnerRefs` all the way
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-app-api': patch
---
I have added default icons for the catalog, scaffolder, techdocs, and search.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-components': patch
---
Add Theme Overrides for Sidebar
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Bumped `@spotify/eslint-config-typescript` from `v10` to `v12`, dropping support for Node.js v12.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/techdocs-common': patch
---
Default TechDocs container used at docs generation-time is now [v0.3.5](https://github.com/backstage/techdocs-container/releases/tag/v0.3.5).
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/techdocs-common': patch
---
Updated to properly join URL segments under any OS for both AWS S3 and GCP
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-app-api': patch
'@backstage/core-plugin-api': patch
---
Improve API documentation for @backstage/core-plugin-api
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-azure-devops': patch
---
Simplified queue time calculation in `BuildTable`.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/core-app-api': patch
---
The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement.
The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`.
The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or MUI, as it allows you to avoid those dependencies completely.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix a bug where only file mode 775 is considered an executable
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
'@backstage/plugin-scaffolder-backend-module-rails': patch
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
---
Add missing API docs to scaffolder action plugins
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-plugin-api': patch
---
Deprecate unused ApiRef types
+3
View File
@@ -52,6 +52,7 @@ configmaps
configs
const
cookiecutter
cron
css
Datadog
dataflow
@@ -162,6 +163,8 @@ Monorepo
monorepos
msgraph
msw
mutex
mutexes
mysql
namespace
namespaced
@@ -7,8 +7,42 @@ on:
jobs:
sync:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: Create Snyk report
uses: snyk/actions/node@master
continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found
@@ -23,3 +57,5 @@ jobs:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Update Github issues
run: yarn ts-node scripts/snyk-github-issue-sync.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+56
View File
@@ -0,0 +1,56 @@
---
id: deprecations
title: Deprecations
description: A list of active and past deprecations
---
## Introduction
This page contains extended documentation for some of the deprecations in
various parts of Backstage. It is not an exhaustive list as most deprecation
only come in the form of a changelog notice and a console warning. The
deprecations listed here are the ones that need a bit more guidance than what
fits in a console message.
### App Theme
`Released 2021-11-12 in @backstage/core-plugin-api v0.1.13`
In order to provide more flexibility in what types of themes can be used and how
they are applied, the `theme` property on the `AppTheme` type is being
deprecated and replaced by a `Provider` property instead. The `Provider`
property is a React component that will be mounted at the root of the app
whenever that theme is active. This also removes the tight connection to MUI and
opens up for other type of themes, and removes the hardcoded usage of
`<CssBaseline>`.
To migrate an existing theme, remove the `theme` property and move it over to a
new `Provider` component, using `ThemeProvider` from MUI to provide the new
theme, along with `<CssBaseline>`. For example a theme that currently looks like
this:
```tsx
const darkTheme = {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
theme: darkTheme,
};
```
Would be migrated to the following:
```tsx
const darkTheme = {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<ThemeProvider theme={darkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
};
```
+19
View File
@@ -109,6 +109,25 @@ const routes = (
);
```
### Using the Search Modal
In `Root.tsx`, add the `SidebarSearchModal` component:
```bash
import { SidebarSearchModal } from '@backstage/plugin-search';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchModal />
<SidebarDivider />
...
```
For more information about using `Root.tsx`, please see
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
## Adding Search to the Backend
Add the following plugins into your backend app:
@@ -172,6 +172,7 @@ 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,
+2 -1
View File
@@ -512,7 +512,8 @@ Usage: backstage-cli config:check [options]
Options:
--package &lt;name&gt; Only load config schema that applies to the given package
--lax Do not require environment variables to be set
--lax Do not require environment variables to be set
--frontend Only validate the frontend configuration
--config &lt;path&gt; Config files to load instead of app-config.yaml (default: [])
-h, --help display help for command
```
+2
View File
@@ -136,6 +136,7 @@ a component:
```ts
export const FooPage = plugin.provide(
createRoutableExtension({
name: 'FooPage',
component: () => import('./components/FooPage').then(m => m.FooPage),
mountPoint: fooPageRouteRef,
}),
@@ -417,6 +418,7 @@ export const myPlugin = createPlugin({
export const MyPage = myPlugin.provide(
createRoutableExtension({
name: 'MyPage',
component: () => import('./components/MyPage').then(m => m.MyPage),
mountPoint: rootRouteRef,
}),
+1
View File
@@ -65,6 +65,7 @@ export const examplePlugin = createPlugin({
// Each extension should also be exported from your plugin package.
export const ExamplePage = examplePlugin.provide(
createRoutableExtension({
name: 'ExamplePage',
// The component needs to be lazy-loaded. It's what will actually be rendered in the end.
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
+1
View File
@@ -73,6 +73,7 @@ export const examplePlugin = createPlugin({
export const ExamplePage = examplePlugin.provide(
createRoutableExtension({
name: 'ExamplePage',
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
mountPoint: rootRouteRef,
+6 -2
View File
@@ -151,7 +151,10 @@
{
"type": "subcategory",
"label": "GitLab",
"ids": ["integrations/gitlab/locations"]
"ids": [
"integrations/gitlab/locations",
"integrations/gitlab/discovery"
]
},
{
"type": "subcategory",
@@ -253,7 +256,8 @@
"type": "subcategory",
"label": "API Reference",
"ids": ["reference/index"]
}
},
"api/deprecations"
],
"Tutorials": [
"tutorials/journey",
+2
View File
@@ -100,6 +100,7 @@ nav:
- Org Data: 'integrations/github/org.md'
- GitLab:
- Locations: 'integrations/gitlab/locations.md'
- Discovery: 'integrations/gitlab/discovery.md'
- Google GCS:
- Locations: 'integrations/google-cloud-storage/locations.md'
- LDAP:
@@ -163,6 +164,7 @@ nav:
- API Reference:
- Guides:
- Utility APIs: 'api/utility-apis.md'
- Deprecations: 'api/deprecations.md'
- Tutorials:
- Future developer journey: 'tutorials/journey.md'
- Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+17
View File
@@ -0,0 +1,17 @@
# @backstage/app-defaults
This package provides a default wiring of a Backstage app that avoids boilerplate when setting up a standard Backstage app.
## Installation
Install the package via Yarn:
```sh
cd packages/app
yarn add @backstage/app-defaults
```
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)
+26
View File
@@ -0,0 +1,26 @@
## API Report File for "@backstage/app-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AppComponents } from '@backstage/core-app-api';
import { AppIcons } from '@backstage/core-app-api';
import { AppOptions } from '@backstage/core-app-api';
import { AppTheme } from '@backstage/core-plugin-api';
import { BackstageApp } from '@backstage/core-app-api';
import { IconComponent } from '@backstage/core-plugin-api';
// @public
export function createApp(
options?: Omit<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
): BackstageApp;
// @public
export type OptionalAppOptions = {
icons?: Partial<AppIcons> & {
[key in string]: IconComponent;
};
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
components?: Partial<AppComponents>;
};
```
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
"version": "0.1.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/app-defaults"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "backstage-cli build --outputs types,esm",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.7.3",
"@backstage/core-app-api": "^0.1.20",
"@backstage/core-plugin-api": "^0.1.13",
"@backstage/theme": "^0.2.13",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"react": "^16.12.0",
"react-router-dom": "6.0.0-beta.0"
},
"devDependencies": {
"@backstage/cli": "^0.8.2",
"@backstage/test-utils": "^0.1.21",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react": "*"
},
"files": [
"dist"
]
}
@@ -0,0 +1,42 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils';
import React, { PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { createApp } from './createApp';
describe('Optional ThemeProvider', () => {
it('should render app with user-provided ThemeProvider', async () => {
const components = {
NotFoundErrorPage: () => null,
BootErrorPage: () => null,
Progress: () => null,
Router: MemoryRouter,
ErrorBoundaryFallback: () => null,
ThemeProvider: ({ children }: PropsWithChildren<{}>) => (
<main role="main">{children}</main>
),
};
const App = createApp({ components }).getProvider();
await renderWithEffects(<App />);
expect(screen.getByRole('main')).toBeInTheDocument();
});
});
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { apis, components, icons, themes } from './defaults';
import {
AppTheme,
BackstagePlugin,
IconComponent,
} from '@backstage/core-plugin-api';
import {
AppComponents,
AppOptions,
AppIcons,
createSpecializedApp,
} from '@backstage/core-app-api';
/**
* Creates a new Backstage App using a default set of components, icons and themes unless
* they are explicitly provided.
*
* @public
*/
export function createApp(
options?: Omit<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
) {
return createSpecializedApp({
...options,
apis: options?.apis ?? [],
bindRoutes: options?.bindRoutes,
components: {
...components,
...options?.components,
},
configLoader: options?.configLoader,
defaultApis: apis,
icons: {
...icons,
...options?.icons,
},
plugins: (options?.plugins as BackstagePlugin<any, any>[]) ?? [],
themes: options?.themes ?? themes,
});
}
/**
* The set of app options that {@link createApp} will provide defaults for
* if they are not passed in explicitly.
*
* @public
*/
export type OptionalAppOptions = {
/**
* A set of icons to override the default icons with.
*
* The override is applied for each icon individually.
*
* @public
*/
icons?: Partial<AppIcons> & {
[key in string]: IconComponent;
};
/**
* A set of themes that override all of the default app themes.
*
* If this option is provided none of the default themes will be used.
*
* @public
*/
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[]; // TODO: simplify once AppTheme is updated
/**
* A set of components to override the default components with.
*
* The override is applied for each icon individually.
*
* @public
*/
components?: Partial<AppComponents>;
};
+274
View File
@@ -0,0 +1,274 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AlertApiForwarder,
NoOpAnalyticsApi,
ErrorApiForwarder,
ErrorAlerter,
GoogleAuth,
GithubAuth,
OAuth2,
OktaAuth,
GitlabAuth,
Auth0Auth,
MicrosoftAuth,
BitbucketAuth,
OAuthRequestManager,
WebStorage,
UrlPatternDiscovery,
SamlAuth,
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
} from '@backstage/core-app-api';
import {
createApiFactory,
alertApiRef,
analyticsApiRef,
errorApiRef,
discoveryApiRef,
oauthRequestApiRef,
googleAuthApiRef,
githubAuthApiRef,
oauth2ApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
auth0AuthApiRef,
microsoftAuthApiRef,
storageApiRef,
configApiRef,
samlAuthApiRef,
oneloginAuthApiRef,
oidcAuthApiRef,
bitbucketAuthApiRef,
atlassianAuthApiRef,
} from '@backstage/core-plugin-api';
export const apis = [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
),
}),
createApiFactory({
api: alertApiRef,
deps: {},
factory: () => new AlertApiForwarder(),
}),
createApiFactory({
api: analyticsApiRef,
deps: {},
factory: () => new NoOpAnalyticsApi(),
}),
createApiFactory({
api: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) => {
const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder());
UnhandledErrorForwarder.forward(errorApi, { hidden: false });
return errorApi;
},
}),
createApiFactory({
api: storageApiRef,
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
createApiFactory({
api: oauthRequestApiRef,
deps: {},
factory: () => new OAuthRequestManager(),
}),
createApiFactory({
api: googleAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GoogleAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: microsoftAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
MicrosoftAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oktaAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OktaAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: gitlabAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GitlabAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: auth0AuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
Auth0Auth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oauth2ApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: samlAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, configApi }) =>
SamlAuth.create({
discoveryApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oneloginAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OneLoginAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oidcAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
provider: {
id: 'oidc',
title: 'Your Identity Provider',
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: bitbucketAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketAuth.create({
discoveryApi,
oauthRequestApi,
defaultScopes: ['team'],
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: atlassianAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
return AtlassianAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
});
},
}),
];
@@ -0,0 +1,38 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { OptionallyWrapInRouter } from './components';
describe('OptionallyWrapInRouter', () => {
it('should wrap with router if not yet inside a router', async () => {
render(<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('should not wrap with router if already inside a router', async () => {
render(
<MemoryRouter>
<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>
</MemoryRouter>,
);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
@@ -0,0 +1,86 @@
/*
* 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 React, { ReactNode } from 'react';
import Button from '@material-ui/core/Button';
import { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components';
import {
MemoryRouter,
useInRouterContext,
BrowserRouter,
} from 'react-router-dom';
import {
AppComponents,
BootErrorPageProps,
ErrorBoundaryFallbackProps,
} from '@backstage/core-plugin-api';
export function OptionallyWrapInRouter({ children }: { children: ReactNode }) {
if (useInRouterContext()) {
return <>{children}</>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
const DefaultNotFoundPage = () => (
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />
);
const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => {
let message = '';
if (step === 'load-config') {
message = `The configuration failed to load, someone should have a look at this error: ${error.message}`;
} else if (step === 'load-chunk') {
message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`;
}
// TODO: figure out a nicer way to handle routing on the error page, when it can be done.
return (
<OptionallyWrapInRouter>
<ErrorPage status="501" statusMessage={message} />
</OptionallyWrapInRouter>
);
};
const DefaultErrorBoundaryFallback = ({
error,
resetError,
plugin,
}: ErrorBoundaryFallbackProps) => {
return (
<ErrorPanel
title={`Error in ${plugin?.getId()}`}
defaultExpanded
error={error}
>
<Button variant="outlined" onClick={resetError}>
Retry
</Button>
</ErrorPanel>
);
};
/**
* Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}.
*
* @public
*/
export const components: AppComponents = {
Progress,
Router: BrowserRouter,
NotFoundErrorPage: DefaultNotFoundPage,
BootErrorPage: DefaultBootErrorPage,
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
};
@@ -35,52 +35,27 @@ import MuiPeopleIcon from '@material-ui/icons/People';
import MuiPersonIcon from '@material-ui/icons/Person';
import MuiWarningIcon from '@material-ui/icons/Warning';
type AppIconsKey =
| 'brokenImage'
| 'catalog'
| 'scaffolder'
| 'techdocs'
| 'search'
| 'chat'
| 'dashboard'
| 'docs'
| 'email'
| 'github'
| 'group'
| 'help'
| 'kind:api'
| 'kind:component'
| 'kind:domain'
| 'kind:group'
| 'kind:location'
| 'kind:system'
| 'kind:user'
| 'user'
| 'warning';
export type AppIcons = { [key in AppIconsKey]: IconComponent };
export const defaultAppIcons: AppIcons = {
brokenImage: MuiBrokenImageIcon,
export const icons = {
brokenImage: MuiBrokenImageIcon as IconComponent,
// To be confirmed: see https://github.com/backstage/backstage/issues/4970
catalog: MuiMenuBookIcon,
scaffolder: MuiCreateNewFolderIcon,
techdocs: MuiSubjectIcon,
search: MuiSearchIcon,
chat: MuiChatIcon,
dashboard: MuiDashboardIcon,
docs: MuiDocsIcon,
email: MuiEmailIcon,
github: MuiGitHubIcon,
group: MuiPeopleIcon,
help: MuiHelpIcon,
'kind:api': MuiExtensionIcon,
'kind:component': MuiMemoryIcon,
'kind:domain': MuiApartmentIcon,
'kind:group': MuiPeopleIcon,
'kind:location': MuiLocationOnIcon,
'kind:system': MuiCategoryIcon,
'kind:user': MuiPersonIcon,
user: MuiPersonIcon,
warning: MuiWarningIcon,
catalog: MuiMenuBookIcon as IconComponent,
scaffolder: MuiCreateNewFolderIcon as IconComponent,
techdocs: MuiSubjectIcon as IconComponent,
search: MuiSearchIcon as IconComponent,
chat: MuiChatIcon as IconComponent,
dashboard: MuiDashboardIcon as IconComponent,
docs: MuiDocsIcon as IconComponent,
email: MuiEmailIcon as IconComponent,
github: MuiGitHubIcon as IconComponent,
group: MuiPeopleIcon as IconComponent,
help: MuiHelpIcon as IconComponent,
'kind:api': MuiExtensionIcon as IconComponent,
'kind:component': MuiMemoryIcon as IconComponent,
'kind:domain': MuiApartmentIcon as IconComponent,
'kind:group': MuiPeopleIcon as IconComponent,
'kind:location': MuiLocationOnIcon as IconComponent,
'kind:system': MuiCategoryIcon as IconComponent,
'kind:user': MuiPersonIcon as IconComponent,
user: MuiPersonIcon as IconComponent,
warning: MuiWarningIcon as IconComponent,
};
@@ -0,0 +1,20 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { apis } from './apis';
export { components } from './components';
export { icons } from './icons';
export { themes } from './themes';
@@ -0,0 +1,50 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { darkTheme, lightTheme } from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { AppTheme } from '@backstage/core-plugin-api';
export const themes: AppTheme[] = [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
theme: lightTheme,
Provider: ({ children }) => (
<ThemeProvider theme={lightTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
{
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
theme: darkTheme,
Provider: ({ children }) => (
<ThemeProvider theme={darkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
];
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Provides the default wiring of a Backstage App
*
* @packageDocumentation
*/
export { createApp } from './createApp';
export type { OptionalAppOptions } from './createApp';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.0",
"@backstage/catalog-model": "^0.9.5",
"@backstage/cli": "^0.8.0",
"@backstage/core-app-api": "^0.1.18",
+2 -2
View File
@@ -26,7 +26,8 @@ import {
RELATION_PART_OF,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { createApp, FlatRoutes } from '@backstage/core-app-api';
import { createApp } from '@backstage/app-defaults';
import { FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
@@ -93,7 +94,6 @@ const app = createApp({
// Custom icon example
alert: AlarmIcon,
},
components: {
SignInPage: props => {
return (
+2 -2
View File
@@ -29,7 +29,7 @@ import LogoIcon from './LogoIcon';
import { NavLink } from 'react-router-dom';
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import { SidebarSearch } from '@backstage/plugin-search';
import { SidebarSearchModal } from '@backstage/plugin-search';
import { Shortcuts } from '@backstage/plugin-shortcuts';
import {
Sidebar,
@@ -79,7 +79,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearch />
<SidebarSearchModal />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
+9
View File
@@ -1,5 +1,14 @@
# @backstage/backend-common
## 0.9.9
### Patch Changes
- 8c4cad0bf2: AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response.
- 0611f3b3e2: Reading app config from a remote server
- Updated dependencies
- @backstage/config-loader@0.7.2
## 0.9.8
### Patch Changes
+3
View File
@@ -374,6 +374,9 @@ export class GitlabUrlReader implements UrlReader {
export { isChildPath };
// @public
export function isDatabaseConflictError(e: unknown): boolean;
// @public
export function loadBackendConfig(options: {
logger: Logger_2;
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.9.8",
"version": "0.9.9",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,7 +31,7 @@
"dependencies": {
"@backstage/cli-common": "^0.1.5",
"@backstage/config": "^0.1.11",
"@backstage/config-loader": "^0.7.1",
"@backstage/config-loader": "^0.7.2",
"@backstage/errors": "^0.1.4",
"@backstage/integration": "^0.6.9",
"@backstage/types": "^0.1.1",
@@ -79,8 +79,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.8.1",
"@backstage/test-utils": "^0.1.20",
"@backstage/cli": "^0.8.2",
"@backstage/test-utils": "^0.1.21",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Knex } from 'knex';
import { omit } from 'lodash';
import { Config, ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import {
createDatabaseClient,
ensureDatabaseExists,
createNameOverride,
ensureDatabaseExists,
normalizeConnection,
} from './connection';
import { PluginDatabaseManager } from './types';
@@ -165,7 +166,7 @@ export class DatabaseManager {
);
return {
// include base connection if client type has not been overriden
// include base connection if client type has not been overridden
...(overridden ? {} : baseConnection),
...connection,
};
@@ -28,3 +28,4 @@ export {
} from './connection';
export type { PluginDatabaseManager } from './types';
export { isDatabaseConflictError } from './util';
@@ -0,0 +1,33 @@
/*
* 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.
*/
/**
* Tries to deduce whether a thrown error is a database conflict.
*
* @public
* @param e - A thrown error
* @returns True if the error looks like it was a conflict error thrown by a
* known database engine
*/
export function isDatabaseConflictError(e: unknown) {
const message = (e as any)?.message;
return (
typeof message === 'string' &&
(/SQLITE_CONSTRAINT: UNIQUE/.test(message) ||
/unique constraint/.test(message))
);
}
@@ -174,27 +174,27 @@ export class ServiceBuilderImpl implements ServiceBuilder {
const server: http.Server = httpsSettings
? await createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
const stoppableServer = stoppable(server, 0);
useHotCleanup(this.module, () =>
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
return new Promise((resolve, reject) => {
app.on('error', e => {
logger.error(`Failed to start up on port ${port}, ${e}`);
function handleStartupError(e: unknown) {
server.close();
reject(e);
}
server.on('error', handleStartupError);
server.listen(port, host, () => {
server.off('error', handleStartupError);
logger.info(`Listening on ${host}:${port}`);
resolve(stoppableServer);
});
const stoppableServer = stoppable(
server.listen(port, host, () => {
logger.info(`Listening on ${host}:${port}`);
}),
0,
);
useHotCleanup(this.module, () =>
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
resolve(stoppableServer);
});
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+35
View File
@@ -0,0 +1,35 @@
# @backstage/backend-tasks
Common distributed task management for Backstage backends.
## Usage
Add the library to your backend package:
```sh
# From your Backstage root directory
cd packages/backend
yarn add @backstage/backend-tasks
```
then make use of its facilities as necessary:
```typescript
import { TaskScheduler } from '@backstage/backend-tasks';
import { Duration } from 'luxon';
const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin');
await scheduler.scheduleTask({
id: 'refresh-things',
frequency: Duration.fromObject({ minutes: 10 }),
fn: async () => {
await entityProvider.run();
},
});
```
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md)
+45
View File
@@ -0,0 +1,45 @@
## API Report File for "@backstage/backend-tasks"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AbortSignal as AbortSignal_2 } from 'node-abort-controller';
import { Config } from '@backstage/config';
import { DatabaseManager } from '@backstage/backend-common';
import { Duration } from 'luxon';
import { Logger as Logger_2 } from 'winston';
// @public
export interface PluginTaskScheduler {
scheduleTask(task: TaskDefinition): Promise<void>;
}
// @public
export interface TaskDefinition {
fn: TaskFunction;
frequency: Duration;
id: string;
initialDelay?: Duration;
signal?: AbortSignal_2;
timeout: Duration;
}
// @public
export type TaskFunction =
| ((abortSignal: AbortSignal_2) => void | Promise<void>)
| (() => void | Promise<void>);
// @public
export class TaskScheduler {
constructor(databaseManager: DatabaseManager, logger: Logger_2);
forPlugin(pluginId: string): PluginTaskScheduler;
// (undocumented)
static fromConfig(
config: Config,
options?: {
databaseManager?: DatabaseManager;
logger?: Logger_2;
},
): TaskScheduler;
}
```
@@ -0,0 +1,64 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
//
// tasks
//
await knex.schema.createTable('backstage_backend_tasks__tasks', table => {
table.comment('Tasks used for scheduling work on multiple workers');
table
.text('id')
.primary()
.notNullable()
.comment('The unique ID of this particular task');
table
.text('settings_json')
.notNullable()
.comment('JSON serialized object with properties for this task');
table
.dateTime('next_run_start_at')
.notNullable()
.comment('The next time that the task should be started');
table
.text('current_run_ticket')
.nullable()
.comment('A unique ticket for the current task run');
table
.dateTime('current_run_started_at')
.nullable()
.comment('The time that the current task run started');
table
.dateTime('current_run_expires_at')
.nullable()
.comment('The time that the current task run will time out');
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
//
// tasks
//
await knex.schema.dropTable('backstage_backend_tasks__tasks');
};
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/backend-tasks"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"scripts": {
"build": "backstage-cli build --outputs cjs,types",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.9.8",
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.4",
"@backstage/types": "^0.1.1",
"@types/luxon": "^2.0.4",
"knex": "^0.95.1",
"lodash": "^4.17.21",
"luxon": "^2.0.2",
"node-abort-controller": "^3.0.1",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"zod": "^3.9.5"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.8",
"@backstage/cli": "^0.8.1",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
"files": [
"dist",
"migrations/**/*.{js,d.ts}"
]
}
@@ -0,0 +1,31 @@
/*
* 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 { resolvePackagePath } from '@backstage/backend-common';
import { Knex } from 'knex';
import { DB_MIGRATIONS_TABLE } from './tables';
const migrationsDir = resolvePackagePath(
'@backstage/backend-tasks',
'migrations',
);
export async function migrateBackendTasks(knex: Knex): Promise<void> {
await knex.migrate.latest({
directory: migrationsDir,
tableName: DB_MIGRATIONS_TABLE,
});
}
@@ -0,0 +1,27 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';
export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';
export type DbTasksRow = {
id: string;
settings_json: string;
next_run_start_at: Date;
current_run_ticket?: string;
current_run_started_at?: Date | string;
current_run_expires_at?: Date | string;
};
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Common distributed task management library for Backstage backends
*
* @packageDocumentation
*/
export * from './tasks';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
@@ -0,0 +1,62 @@
/*
* 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 { getVoidLogger } from '@backstage/backend-common';
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { Duration } from 'luxon';
import waitForExpect from 'wait-for-expect';
import { migrateBackendTasks } from '../database/migrateBackendTasks';
import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl';
describe('PluginTaskManagerImpl', () => {
const databases = TestDatabases.create({
ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
async function init(databaseId: TestDatabaseId) {
const knex = await databases.init(databaseId);
await migrateBackendTasks(knex);
const manager = new PluginTaskSchedulerImpl(
async () => knex,
getVoidLogger(),
);
return { knex, manager };
}
// This is just to test the wrapper code; most of the actual tests are in
// TaskWorker.test.ts
describe('scheduleTask', () => {
it.each(databases.eachSupportedId())(
'can run the happy path, %p',
async databaseId => {
const { manager } = await init(databaseId);
const fn = jest.fn();
await manager.scheduleTask({
id: 'task1',
timeout: Duration.fromMillis(5000),
frequency: Duration.fromMillis(5000),
fn,
});
await waitForExpect(() => {
expect(fn).toBeCalled();
});
},
60_000,
);
});
});
@@ -0,0 +1,50 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Knex } from 'knex';
import { Logger } from 'winston';
import { TaskWorker } from './TaskWorker';
import { PluginTaskScheduler, TaskDefinition } from './types';
import { validateId } from './util';
/**
* Implements the actual task management.
*/
export class PluginTaskSchedulerImpl implements PluginTaskScheduler {
constructor(
private readonly databaseFactory: () => Promise<Knex>,
private readonly logger: Logger,
) {}
async scheduleTask(task: TaskDefinition): Promise<void> {
validateId(task.id);
const knex = await this.databaseFactory();
const worker = new TaskWorker(task.id, task.fn, knex, this.logger);
await worker.start(
{
version: 1,
initialDelayDuration: task.initialDelay?.toISO(),
recurringAtMostEveryDuration: task.frequency.toISO(),
timeoutAfterDuration: task.timeout.toISO(),
},
{
signal: task.signal,
},
);
}
}
@@ -0,0 +1,83 @@
/*
* 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 { Knex } from 'knex';
import { Duration } from 'luxon';
import { AbortSignal } from 'node-abort-controller';
import { Logger } from 'winston';
import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables';
import { sleep } from './util';
/**
* Makes sure to auto-expire and clean up things that time out or for other
* reasons should not be left lingering.
*/
export class PluginTaskSchedulerJanitor {
private readonly knex: Knex;
private readonly waitBetweenRuns: Duration;
private readonly logger: Logger;
constructor(options: {
knex: Knex;
waitBetweenRuns: Duration;
logger: Logger;
}) {
this.knex = options.knex;
this.waitBetweenRuns = options.waitBetweenRuns;
this.logger = options.logger;
}
async start(abortSignal?: AbortSignal) {
while (!abortSignal?.aborted) {
try {
await this.runOnce();
} catch (e) {
this.logger.warn(`Error while performing janitorial tasks, ${e}`);
}
await sleep(this.waitBetweenRuns, abortSignal);
}
}
private async runOnce() {
// SQLite currently (Oct 1 2021) returns a number for returning()
// statements, effectively ignoring them and instead returning the outcome
// of the delete() - and knex also emits a warning about that fact, which
// is why we avoid that entirely for the sqlite3 driver.
// https://github.com/knex/knex/issues/4370
// https://github.com/mapbox/node-sqlite3/issues/1453
const dbNull = this.knex.raw('null');
const tasksQuery = this.knex<DbTasksRow>(DB_TASKS_TABLE)
.where('current_run_expires_at', '<', this.knex.fn.now())
.update({
current_run_ticket: dbNull,
current_run_started_at: dbNull,
current_run_expires_at: dbNull,
});
if (this.knex.client.config.client === 'sqlite3') {
const tasks = await tasksQuery;
this.logger.warn(`${tasks} tasks timed out and were lost`);
} else {
const tasks = await tasksQuery.returning(['id']);
for (const { id } of tasks) {
this.logger.warn(`Task timed out and was lost: ${id}`);
}
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DatabaseManager, getVoidLogger } from '@backstage/backend-common';
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { Duration } from 'luxon';
import { TaskScheduler } from './TaskScheduler';
import waitForExpect from 'wait-for-expect';
describe('TaskScheduler', () => {
const logger = getVoidLogger();
const databases = TestDatabases.create({
ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
async function createDatabase(
databaseId: TestDatabaseId,
): Promise<DatabaseManager> {
const knex = await databases.init(databaseId);
const databaseManager: Partial<DatabaseManager> = {
forPlugin: () => ({
getClient: async () => knex,
}),
};
return databaseManager as DatabaseManager;
}
it.each(databases.eachSupportedId())(
'can return a working plugin impl, %p',
async databaseId => {
const database = await createDatabase(databaseId);
const manager = new TaskScheduler(database, logger).forPlugin('test');
const fn = jest.fn();
await manager.scheduleTask({
id: 'task1',
timeout: Duration.fromMillis(5000),
frequency: Duration.fromMillis(5000),
fn,
});
await waitForExpect(() => {
expect(fn).toBeCalled();
});
},
60_000,
);
});

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