From 4f1c30c1766263e7dd1276445933db00464771b0 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Mon, 4 Oct 2021 15:36:50 -0400 Subject: [PATCH 001/245] Support optional [path] argument in create-app command-line utility When this is specified, then copy the newly created application to the specified directory, otherwise, copy the application to a folder based on the name specified name. Signed-off-by: Colton Padden --- .changeset/neat-rings-protect.md | 5 +++++ packages/create-app/src/createApp.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 .changeset/neat-rings-protect.md diff --git a/.changeset/neat-rings-protect.md b/.changeset/neat-rings-protect.md new file mode 100644 index 0000000000..16b0a5fe82 --- /dev/null +++ b/.changeset/neat-rings-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Support optional path argument when generating a project using `create-app` diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 1da3139f41..0c1cd09d5b 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -77,7 +77,7 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination).catch(error => { + await fs.move(tempDir, destination, { overwrite: true }).catch(error => { throw new Error( `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, ); @@ -119,14 +119,24 @@ export default async (cmd: Command): Promise => { const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); - const appDir = resolvePath(paths.targetDir, answers.name); + + // Use `[path]` argument as applicaiton directory when specified, otherwise + // create a directory using `answers.name` + const appDir = + cmd.args.length > 0 + ? resolvePath(cmd.args[0]) + : resolvePath(paths.targetDir, answers.name); Task.log(); Task.log('Creating the app...'); try { - Task.section('Checking if the directory is available'); - await checkExists(paths.targetDir, answers.name); + // Directory must not already exist when using consructed path from + // `answers.name` + if (cmd.args.length === 0) { + Task.section('Checking if the directory is available'); + await checkExists(paths.targetDir, answers.name); + } Task.section('Creating a temporary app directory'); await createTemporaryAppFolder(tempDir); From 448b3ed9fadcb80fefdb7cca5f4d3f26c07471d1 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 09:36:58 -0400 Subject: [PATCH 002/245] modify changeset from patch to minor as we are in a 0.x release Signed-off-by: Colton Padden --- .changeset/neat-rings-protect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/neat-rings-protect.md b/.changeset/neat-rings-protect.md index 16b0a5fe82..9242d4ce6c 100644 --- a/.changeset/neat-rings-protect.md +++ b/.changeset/neat-rings-protect.md @@ -1,5 +1,5 @@ --- -'@backstage/create-app': minor +'@backstage/create-app': patch --- Support optional path argument when generating a project using `create-app` From fe48f0ea9563687b884f38dfb2fc38ef10bb4771 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 09:41:38 -0400 Subject: [PATCH 003/245] update create-app usage to include optional [path] argument Signed-off-by: Colton Padden --- packages/create-app/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 98a3cee908..c603592207 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -26,10 +26,11 @@ import { version } from '../package.json'; import createApp from './createApp'; const main = (argv: string[]) => { - program.name('backstage-create-app').version(version); - program - .description('Creates a new app in a new directory') + .name('backstage-create-app') + .version(version) + .description('Creates a new app in a new directory or specified [path]') + .usage('[path] [options]') .option( '--skip-install', 'Skip the install and builds steps after creating the app', From 973678fd8cda95f932dc04c7abc075486c86f769 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 11:20:35 -0400 Subject: [PATCH 004/245] use --path option instead of [path] argument in create-app Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 11 +++++------ packages/create-app/src/index.ts | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 0c1cd09d5b..4f0c141d86 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -120,12 +120,11 @@ export default async (cmd: Command): Promise => { const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); - // Use `[path]` argument as applicaiton directory when specified, otherwise + // Use `--path` argument as applicaiton directory when specified, otherwise // create a directory using `answers.name` - const appDir = - cmd.args.length > 0 - ? resolvePath(cmd.args[0]) - : resolvePath(paths.targetDir, answers.name); + const appDir = cmd.path + ? resolvePath(cmd.path) + : resolvePath(paths.targetDir, answers.name); Task.log(); Task.log('Creating the app...'); @@ -133,7 +132,7 @@ export default async (cmd: Command): Promise => { try { // Directory must not already exist when using consructed path from // `answers.name` - if (cmd.args.length === 0) { + if (!cmd.path) { Task.section('Checking if the directory is available'); await checkExists(paths.targetDir, answers.name); } diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index c603592207..1f37be56d2 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -29,8 +29,11 @@ const main = (argv: string[]) => { program .name('backstage-create-app') .version(version) - .description('Creates a new app in a new directory or specified [path]') - .usage('[path] [options]') + .description('Creates a new app in a new directory or specified path') + .option( + '--path [directory]', + 'Location to store the app defaulting to a new folder with the app name', + ) .option( '--skip-install', 'Skip the install and builds steps after creating the app', From 1d7078df932415a798fe70d851e34de040711a68 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 16:10:07 -0400 Subject: [PATCH 005/245] validate path directory, and prevent target from being overwritten Validation now occurs to ensure that the target path exists and is a directory. Additionally, instead of moving the temporary directory to the target path, files are now recursively copied, and then cleaned up. This was to support app creation in the current directory, without clobbering existing files (for example .git/) Instead of overwriting the target `path` directory while using the move method, Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 49 ++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 4f0c141d86..732e2e84d9 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -24,10 +24,11 @@ import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; +import recursive from 'recursive-readdir'; const exec = promisify(execCb); -async function checkExists(rootDir: string, name: string) { +async function checkAppPathDoesntExist(rootDir: string, name: string) { await Task.forItem('checking', name, async () => { const destination = resolvePath(rootDir, name); @@ -40,6 +41,22 @@ async function checkExists(rootDir: string, name: string) { }); } +async function checkPathExistsAndIsDirectory(path: string) { + await Task.forItem('checking', path, async () => { + if (await fs.pathExists(path)) { + if (!fs.lstatSync(path).isDirectory()) { + throw new Error( + 'Directory specified with --path argument is a file\nPlease ensure target path is a directory', + ); + } + } else { + throw new Error( + 'Directory specified with --path argument does not exist\nPlease try again with a different path', + ); + } + }); +} + async function createTemporaryAppFolder(tempDir: string) { await Task.forItem('creating', 'temporary directory', async () => { try { @@ -77,11 +94,18 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination, { overwrite: true }).catch(error => { - throw new Error( - `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, - ); + const tempFiles = await recursive(tempDir).catch(error => { + throw new Error(`Failed to read temporary directory: ${error.message}`); }); + + for (const tempFile of tempFiles) { + const destFile = tempFile.replace(tempDir, destination); + await fs.copy(tempFile, destFile).catch(error => { + throw new Error( + `Failed to move file from ${tempFile} to ${destFile}: ${error.message}`, + ); + }); + } }); } @@ -130,11 +154,13 @@ export default async (cmd: Command): Promise => { Task.log('Creating the app...'); try { - // Directory must not already exist when using consructed path from - // `answers.name` - if (!cmd.path) { - Task.section('Checking if the directory is available'); - await checkExists(paths.targetDir, answers.name); + Task.section('Checking if the directory is available'); + if (cmd.path) { + await checkPathExistsAndIsDirectory(appDir); + } else { + // Directory must not already exist when using consructed path from + // `answers.name` + await checkAppPathDoesntExist(paths.targetDir, answers.name); } Task.section('Creating a temporary app directory'); @@ -146,6 +172,9 @@ export default async (cmd: Command): Promise => { Task.section('Moving to final location'); await moveApp(tempDir, appDir, answers.name); + Task.section('Cleanup'); + await cleanUp(tempDir); + if (!cmd.skipInstall) { Task.section('Building the app'); await buildApp(appDir); From 6c93f322ca38af6113854e150eac137d2b80c6d5 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 7 Oct 2021 11:00:40 -0400 Subject: [PATCH 006/245] use resolvePath with targetDir when using cmd.path Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 732e2e84d9..bc33fd91ae 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -147,7 +147,7 @@ export default async (cmd: Command): Promise => { // Use `--path` argument as applicaiton directory when specified, otherwise // create a directory using `answers.name` const appDir = cmd.path - ? resolvePath(cmd.path) + ? resolvePath(paths.targetDir, cmd.path) : resolvePath(paths.targetDir, answers.name); Task.log(); From bd6f4fb2d95442395a34add23aec43b4ef643ce6 Mon Sep 17 00:00:00 2001 From: Suzanne Daniels Date: Thu, 7 Oct 2021 16:38:48 +0200 Subject: [PATCH 007/245] Getting started guide rewrite Signed-off-by: Suzanne Daniels --- .../bs-getting-started-portal.png | Bin 0 -> 134243 bytes .../bs-getting-started-startup.png | Bin 0 -> 108170 bytes docs/assets/getting-started/bs-wizard.png | Bin 0 -> 20559 bytes docs/getting-started/index.md | 100 ++++++++++++------ 4 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 docs/assets/getting-started/bs-getting-started-portal.png create mode 100644 docs/assets/getting-started/bs-getting-started-startup.png create mode 100644 docs/assets/getting-started/bs-wizard.png diff --git a/docs/assets/getting-started/bs-getting-started-portal.png b/docs/assets/getting-started/bs-getting-started-portal.png new file mode 100644 index 0000000000000000000000000000000000000000..f16ad447578e8dea7fa38153f0dabeb6b405e452 GIT binary patch literal 134243 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ℑ+j8SaEY2kXTA>Oa_e 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 B-(jaB!iTr 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 Ti11GA06$}Wb3HQhbmad4>|=xw literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/bs-getting-started-startup.png b/docs/assets/getting-started/bs-getting-started-startup.png new file mode 100644 index 0000000000000000000000000000000000000000..626f7b129737e512a36bfa168a7b73bfe201df08 GIT binary patch literal 108170 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 literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/bs-wizard.png b/docs/assets/getting-started/bs-wizard.png new file mode 100644 index 0000000000000000000000000000000000000000..679878e252d7f2c8c47eedcf1d610c0a297d8cbb GIT binary patch literal 20559 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 literal 0 HcmV?d00001 diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index b01afac5c3..3367fa2572 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -4,49 +4,87 @@ title: Getting Started description: Documentation on How to get started with Backstage --- -There are two different ways to get started with Backstage: +For most Backstage installations, installing the standalone app will bring +you the best and most streamlined experience. In this guide you will: -- **Recommended:** Create a standalone app -- **Contributors:** Clone the Backstage repository +- Deploy Backstage Standalone with npm packages +- Run Backstage Standalone with a SQLite in-memory database and demo content -Creating a standalone app makes it simpler to customize the application for your -needs and stay up to date with the project. You will depend on `@backstage` -packages from npm, making your app much smaller. This is the recommended -approach for most installations. +This guide assumes a basic understanding of working on a Linux based operating +system using tools like apt-get, npm, yarn, curl. Docker knowledge is also +helpful for making the best use of your Backstage installation. -If you want to contribute plugins or to the project in general, it's easier to -fork and clone the repository. The `@backstage` packages will be included in the -clone. That will let you stay up to date with the latest changes, and give you -an easier path to make Pull Requests. +If you are planning to contribute plugins or the project in general, we advise +you to use the [Getting Started for Contributers](https://backstage.io/docs/getting-started/running-backstage-locally) guide to +do a repository-based installation. + +### Prerequisites + +- Access to a linux based operating system +- An account with elevated rights (eg. via sudo) +- `curl` or `wget` installed +- Node.js Active LTS Release installed (currently v14) using one of these methods: + - Using `nvm` (recommended) + - [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script) + - [Install and change Node version with nvm](https://nodejs.org/en/download/package-manager/#nvm) + - [Binary Download](https://nodejs.org/en/download/) + - [Package manager](https://nodejs.org/en/download/package-manager/) + - [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md) +- `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install) +- `docker` [installation](https://docs.docker.com/engine/install/) +- `git` [installation](https://github.com/git-guides/install-git) +- If the system is not directly accessible over your network, the following + ports need to be opened: 3000, 7000 ### Create your Backstage App -Backstage provides the `@backstage/create-app` package to scaffold standalone -instances of Backstage. You will need to have -[Node.js](https://nodejs.org/en/download/) Active LTS Release installed -(currently v14) and [Yarn](https://classic.yarnpkg.com/en/docs/install). You -will also need to have [Docker](https://docs.docker.com/engine/install/) -installed to use some features like Software Templates and TechDocs. - -Using `npx` you can then run the following to create an app in a chosen -subdirectory of your current working directory: +To install the Backstage Standalone app, we make use of `npx`, a tool to run Node executables +straight from the registry. Running the command below will install Backstage. The wizard will +create a subdirectory inside your current working directory. ```bash npx @backstage/create-app ``` -You will be taken through a wizard to create your app. You can read more about -this process in [Create an app](./create-an-app.md). +The wizard will ask you -### Contributing to Backstage +- The name of the app, which will also be the name of the directory +- The database type to use for the backend. For this guide, you'll be using the SQLite option. -If you intend to make changes to the core project's packages, certain plugins, -or project documentation, then you can fork and clone -[https://github.com/backstage/backstage](https://github.com/backstage/backstage). +

+ Screenshot of the wizard asking for a name for the app, and a selection menu for the database. +

-This will let you run the latest code off of the `master` branch, fix bugs or -contribute new features, run test suites, etc. +### Run the Backstage app -You can read more in our -[CONTRIBUTING](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) -guide, which can help you get setup with a Backstage development environment. +When the installation is complete you can go to the application directory and start the app. The `yarn dev` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window. + +```bash +cd my-backstage-app +yarn dev +``` + +

+ Screenshot of the command output, with the message webpack compiled successfully. +

+ +It might take a little while, but as soon as the message `[0] webpack compiled successfully` appears, you can open a browser and directly navigate to +your freshly installed Backstage portal at `http://localhost:3000`. You can start exploring the demo immediately. + +

+ Screenshot of the Backstage portal homescreen. +

+ +Congratulations! That should be it. Let us know how it went: +[on discord](https://discord.gg/EBHEGzX), file issues for any +[feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) +or +[plugin suggestions](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), +or [bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md) +you have, and feel free to [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! + +The most common next steps are to configure Backstage, add a plugin and moving to a more persistend database: + +- [Setting up Authentication](https://github.com/backstage/backstage/tree/master/plugins/auth-backend#github) +- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres) +- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins) From 91ae05b8ec4c77aa7a1bebb5bbc49573415b0ec5 Mon Sep 17 00:00:00 2001 From: Suzanne Daniels Date: Thu, 7 Oct 2021 17:07:30 +0200 Subject: [PATCH 008/245] rename img to match styleguide Signed-off-by: Suzanne Daniels --- docs/assets/getting-started/portal.png | Bin 0 -> 134243 bytes docs/assets/getting-started/startup.png | Bin 0 -> 108170 bytes docs/assets/getting-started/wizard.png | Bin 0 -> 20559 bytes docs/getting-started/index.md | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 docs/assets/getting-started/portal.png create mode 100644 docs/assets/getting-started/startup.png create mode 100644 docs/assets/getting-started/wizard.png diff --git a/docs/assets/getting-started/portal.png b/docs/assets/getting-started/portal.png new file mode 100644 index 0000000000000000000000000000000000000000..f16ad447578e8dea7fa38153f0dabeb6b405e452 GIT binary patch literal 134243 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ℑ+j8SaEY2kXTA>Oa_e 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 B-(jaB!iTr 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 Ti11GA06$}Wb3HQhbmad4>|=xw literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/startup.png b/docs/assets/getting-started/startup.png new file mode 100644 index 0000000000000000000000000000000000000000..626f7b129737e512a36bfa168a7b73bfe201df08 GIT binary patch literal 108170 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 literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/wizard.png b/docs/assets/getting-started/wizard.png new file mode 100644 index 0000000000000000000000000000000000000000..679878e252d7f2c8c47eedcf1d610c0a297d8cbb GIT binary patch literal 20559 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 literal 0 HcmV?d00001 diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 3367fa2572..ab217d7655 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -52,7 +52,7 @@ The wizard will ask you - The database type to use for the backend. For this guide, you'll be using the SQLite option.

- Screenshot of the wizard asking for a name for the app, and a selection menu for the database. + Screenshot of the wizard asking for a name for the app, and a selection menu for the database.

### Run the Backstage app @@ -65,14 +65,14 @@ yarn dev ```

- Screenshot of the command output, with the message webpack compiled successfully. + Screenshot of the command output, with the message webpack compiled successfully.

It might take a little while, but as soon as the message `[0] webpack compiled successfully` appears, you can open a browser and directly navigate to your freshly installed Backstage portal at `http://localhost:3000`. You can start exploring the demo immediately.

- Screenshot of the Backstage portal homescreen. + Screenshot of the Backstage portal homescreen.

Congratulations! That should be it. Let us know how it went: From 9daaf366c1958d2db83876682220aaaacf308fff Mon Sep 17 00:00:00 2001 From: Suzanne Daniels Date: Thu, 7 Oct 2021 17:08:37 +0200 Subject: [PATCH 009/245] Delete imgs Signed-off-by: Suzanne Daniels --- .../bs-getting-started-portal.png | Bin 134243 -> 0 bytes .../bs-getting-started-startup.png | Bin 108170 -> 0 bytes docs/assets/getting-started/bs-wizard.png | Bin 20559 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/assets/getting-started/bs-getting-started-portal.png delete mode 100644 docs/assets/getting-started/bs-getting-started-startup.png delete mode 100644 docs/assets/getting-started/bs-wizard.png diff --git a/docs/assets/getting-started/bs-getting-started-portal.png b/docs/assets/getting-started/bs-getting-started-portal.png deleted file mode 100644 index f16ad447578e8dea7fa38153f0dabeb6b405e452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134243 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ℑ+j8SaEY2kXTA>Oa_e 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 B-(jaB!iTr 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 Ti11GA06$}Wb3HQhbmad4>|=xw diff --git a/docs/assets/getting-started/bs-getting-started-startup.png b/docs/assets/getting-started/bs-getting-started-startup.png deleted file mode 100644 index 626f7b129737e512a36bfa168a7b73bfe201df08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108170 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 diff --git a/docs/assets/getting-started/bs-wizard.png b/docs/assets/getting-started/bs-wizard.png deleted file mode 100644 index 679878e252d7f2c8c47eedcf1d610c0a297d8cbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20559 zcmZU)19T<9w=X=glZkEHwrx9^=)|^d+qP|MVp|j2p6JW`@4fHc^}e%KovyBp>e{tS zzug_KASVt7jRpPV#}7D32@$0qKY%*E^Lmir-~XzrbH(2Uyyn8f3X;OY1PTtersh^A zKYmce8yOfNNzhRIHZ(La_&r5U3GLvj6c!exWZ*mAH{1t3)Br0LJ~j*%0|dk;Knlx% zh3eHg(g#^Ue;zPdfx?*4#d$FPaIukqNRD^SI-(K_+@_=yndtw3Par2nM?v+iROUxR z@`NQV!the! z`uwMWAEEMf@$PZ$lt85k@KQ?DQUXnmK7uBSrXFJ90mKYJN~k>d36D-FQLs_4m>2@P zaEjiyP$W1z*@9sv=!^^)xgbnF1Vc>zbImPr{(`4yLNb&&{(@IOK?eVp1q>C4w1Hss z)}xeSVhgTPWx>J<^?!Z&-kKW3c|bRr4S#+0_I!SQ@qvaG{mS^Z zr<#d|q^Ydz5327x0r7oB`c5Kw!2d%9>c|86pZpKK ze~g04!jh8TS7jpy6B8RpfUT3HcToSgswHz34JQp*87?DRYdS+?+g~PhZq|1HM1JtN zaeZg4O`Hq~+^nr^9J$MtlK;^oV&Z7zU~cDRZfisEPp{!GTW2R;BBFm4{h!}|J*SDA`TwoS#__+K z_1!@Fe-wH~ItKdxqx)Nw=U*w8g1MWCm8OWf^|yJx>)_*L;rS2y|Bvv0EBs%A8vj?2 zg@x&VOa3p)|0}8LXyPDjYyDlL6W{;snEw|3-^BkG4Oj>4`wObmB@%N``cI>(ReBa%G2D!>2^l=yn#^?(|5=N%p5qW!Ae~YE@~s z%FXrmnzIxRSK0V3m`9u0_wD%&`+tGKhh$}1Mv6=Z%EkXueB4lm!p5z7T+&usvZ1$kq^QR0fXny8TL)|@^ zGYl&@g;XGnP!;ldPRk8nMKT3AE1HOXXqd~}6Xa_{MhZ4tkG@6_SMpi92LIj1!Hl2_sd>n+kFl9?V4Gx;ZPW2+d+c#*&_p1?0Oz% zPzA#BO4&^PC?3pbR9_kX#^}gMfo7vA$6gpQgUiJ#7sce?`rS7B{kT?ZYBU-Rg;IQa zp~vbKLJ@poFSvA12COEg!rLZ~!nbLUMy~3wPrl{3llf_j=zwGHflNK+tYe|Oa%shm zt{>YK`*ANLHc9e25ie|G6gy6!N;VsyGrlebPA&*-Z{*AQyl){qNxdDYCXSwij&1?9 za?AHxo6R7}U)vQmFs;vl*6OT0*1rz4nrdm6BrY3+N*O96uhTuXZjWow zc3a@KZ6g%hOF&Tr79o*YJUz z@^)O=nDxKEx?na#hbcR(QSlCRsPEqlQahGEXK6Y<;#2BsYlY7I_~RMaU^a3sz-%*w zS?g^pg`QU7>YT#{Tg)$2Fx5HC2O3p9;!_$mlGEpq_{&jUHse(T8^yFD2j#1>MxG(3 z(SsKcxM51N`EY-Z#{eW%YXGLX0h0A>vUg~Nz&+Dbh?@X zUhV)pJ-j|IyAZ}^X8YoE2|b^GEoTj>QQq%!zlK_@H)gA~*@s3)_gi;-1|4U3OL6+~ zeBbR=N1KTU%#X@5Ws;I-jc@ z93NM*NvDqYx??&{rZKvmSGTIH)fo=j9i^L5)t+DXe&Nw~KP1do&~@Di;kYj<0!Xz8 zn|qFPeerxhUrWz~J*K&@d2n1e?JIUFR4Z@pCg`cJO?eqvBJ~`nSS#?~T;pPap19v{ z1{L-6YTQ2lp4V2kZ@au*0&>#JUv4&gzoG{MfH&<&aiOy@X?Z;!`c6x8Ye2JPHGTaa zv6uG~<@p3dAbY3}1B>)~eJE&Yhu^MyYlFIT#A>D#Gd<4AOW&VotbBZtdydC>N_wC1 zLSQii^twGLX{em%(D}32M&0 zTu{3<5q}bKqx)HLGlL>HNZosBwxRZ=Az z%YPQ5VBR53x!J@b8E}6pc8VQEBR9rFwkOR*>}p&k+-nTQ&Zuooow;~XW@OH$v$Udk zxvRlSiwf8ZI5c_+iB~g*qp(P@?OS0zT1dWPfI+{nKTl=CjL@5aa$klXB-ok zzbkMM=DII+RT7E`V7$Hmm8VrQpEb~qwnz7S|rnJq-#<)72Ux7c+o; z@EI}hd;4`KgE5Ch%iqjc$a^=;?Jy*9bNCYymsw6!NT<5Rrjc_ACZ4P>QH}?y+w)N^ zL!_<5x98&Z@pBkm|IhbII5b9{4Vy6y$-=5c>@KfY!19spF<$1|e}5XI zl%=V%wnwWum)o_53+p5Q2iNToa&lZ2k9&g__uIF>{KQ6%+}zYzs`l=DvfxQ^UNd-R`{J%MZm1*ti^dT+y!`e>Ho5xs}pL7*W^vR)rFSR^W}h>b&mJhGNNZf=Z1b zTdQ!k~@%}9vAoLO@fMsW{8@Ig57$(NT!!3m&>^VKO2$u{SSdIBO{~RO+Q4v z!7gx8Qp%s2&MPJ0do9FM* zgp|w>+{(@mAA-v@;_E>y2J9ICeEyM5h%z?&>KuTlh1FwPXbBA4%s2IwB$=(E%NUYW zcQ4-VnQWDeR=8Zv%z@_3i5%z7v3F6jS<0OoANBsJ!f_`0{AX?=>_(c?>PiFy^5!Cz zj<)wvLE9RsolYATZH&a|a;-?-Lkh$f?~XzL*&f=23pnEjXG#*G{hnA+5l8(;Y7IpP z7xGoxQ5jZ76$`5^4Ea+|ZcLUUU%oP_P}0vgYs4({T0v7&9GB|T@a|F)!dFH2=AAAe z#;N_ldCAMcqdTJJ+Utp=v@%AEKUavcxHC7PB;^c_F!?*+%i7vDp%x_TJXL zp+!^4n04V*6&?U)iUt;jg5kMTtwuP^YqY$x?I{47*=SB><{%>p*b{$th*x-f!%tCZ z&v3sgL=IMv<~9qM@#B}PL^hZ1!hW1RF6AI5w8W5 zzh_(&r$)v5^7^kjJ;-1Iid%($N}piCVey|1f6Y~x&ZJidg)dXG=9AJ{-Isbrt_t#6 zA6-e$^4+2wkVOqhS32I>&T*BUDqwRRUpOYE+}#W!yfu@<$=B^?+V+%#>PDi`GPq1? zJPsWA$OE67PNwAt5v+7lfR=}QfBT1&0`JQV#G&?Y7z;ELlSkR+zqb?zWrE&sm#qL= z;G>uQSamGy4ZxeJzW1dnD_-+xzHlg&Y@!xfbEk-VzOv)tOKCKZ>}PaIURxVRX^sb# zT8&P*n}80=x<{T}eu`-SV;=}YURjxhDmi3N(gDh&p#b3+BWqrZRF@9-K|5X)Aitcc zR5WG}^Oh3_9u|*9!+Z^_01^I#{|CO;i+={bY^F})ffm)P;2)S=9{1f~^u@OIG}HS+ zHEm7Ja+&4lE!W(6@bYX^W8<_KGZu)u5x`Zxou2EKv(#@(r+bO~oYYh^kFV9~6HXY_ z(Z3*2&Y-$~gPJMaRD-5oWRiX(SW^ADcw^o)DHsUy8(I)8uAJHphL& zffgb83TNt(;hV2k;V&z!mWi1jIho>7n$XU!5VEDpD|}|HiX&7hBltQU$xa zj=RRKF#hUxA(SZkMP6%n2L$DFXf9iityd~gAm6AX0~h>FAUwHXWyA#kzN*Rn_-5JBY#hS{7KQQyC@4%}MnJMnMf(+%HA?&z;s@%^&9|OoQD_-{Fp<#7E@Lj_*-l6;*Q+&6iqW=<0j~>w z`0yEd*sK;wd(6Rc;JcHt)|~9y_%)tLTwx`0kZgFen_}-O*|49pg{ePb$&c$SSLmGi zAGXFIm~+gno8RzWvg)_1>nnIK8Qn3(~MsGjifjdntBf>je4ljk*$v`d+2R7aok!Goj(`lX(5lUqnA&oE9`CBCJD{Qe)L= z_F)u}RTcpu2>3B_*m)TLucyA+f$S8oDQyr%{Av~Y1fTp6o%6;#r*h>cM5DI}$ z1tV=lsznf3i07-=&8Bfo=y=b57vAhi)9>BS@Q?oK543-?Zmm4Zx-Xf^jLXcdyw*GR z`unyprwYmswjP7a{ch=r&1Tit{k&ay+Sl*xdIpMc4VvKi1a<|&!^5FE}xFuzmWOsm9e@yV+GIlw{|ZG$(SyK z(e>|_=wnE(jFjVD^stkrPcKX3Enz)-E{RsyiX!sra>>q3Qd>o2VCI6Lz3JP}R9*|P zw{gHyUzcev?gnDf%EH&4B0wV3Kq{xD#1GR?S6?I2;0{HtH%|RqO>yJbafX`ArZ^IzYHf+D@JQV_GJcc}lM!bNOH$3ScdUygF~PVj?ql^P@H zfn-+oUV7D$B8`|3(y?O(TfPw-;kVa0C%!J7xg~jOaFRelHSl#v^Qf~jWD1wv?jcC40(dZ8>^4I2 z_8>|F-xm8=rC)EeVrWeb#3GmJv3we8#d)#WhCZs{_G&FoU3H*UN6}v!XLw*T0;2L3 zMHlL>i6iOVEr=eL3)x)n#>fkGJ6-m?Uqcn2>n6-?ijYTxppXyj>nK-zEtJ&t+=~Hf z@S6@3v@hoU@@;z&)JdmPu#B>m>3al%d2HgeG3T0~Hb4Xl5_Y;V`<%FPG@1=Vwj)NK z(~%hn{*I)xGOL}gwuf61p|JDI`NEg?Vn7dNZe9*vHK~e`1uhO7)~&n0iDOmVE||!H zr=qcLgbU4Pax8SCEt65iGM+3&U|88LNQ=U;;Df5g=ms zJJw9r%6JIjP>+90hfSkZjw^Z}Lc8hWk+?{UE%h0zL?K5!CawJ6yALPOHIoNzvhvsz#`?-Ov6($A4YO5#EkbSk-5cFeEl?*EftbU z%xS5Y@HBvkpie@t;m}LUN+@U{oFybZ>VUhUp%)$&9dRo8Pzj%RWL6*ruJke};82i= zn#rBm{nR|7GvZv1RC^h4De_eeNtnpU7m9 ziKOS1UltZ+rpBam#7uxJtpf4L-La999xcr znn--+`nktNS|B9TOKK#ty12Pf^lD;PRV5I9GYNn<-M`e6ZEWCZ4lB{|kfYkeU%SG0 zxn9v#@KR((oF{rXRA-`tQ@xWJl415C!gC%gl^jQL1fRw^HlcFFkipcP8ifd-!A>coR;yRtMQNT= zpbWL5dIH-2xd|*_D1J}SP4zmTYj*|8bAQ2{7YMa7x*9^S#T<W0#zHaTlF+bAwy zR8mX@v{Kr7@#=YO-OMKn#PaI}(&=(pwL0D2JC0u0qODL4yLe=cjBMZ~$c+@EO-~#s zg?b?8`(32g^L}LFx&4FB0Z#k&3Q=kB9;U2aMk`ge zP@5cw=>rBV27`ckA8qrsPf;|@)|N3@+%8#d6?l<7Vu zubGO=H-tE{k$XsVd^XY%B^fKKBmN5UmFWfsrjfljEV`3JWZK9Y93kd&2#8z|6S+5( zIZX$W4;Qc2w}%@?I~Q1z>)xF#Yjx*r{!z=ppSMeJPcSfY&7!0{=xuK5G~)cT6l+aGr#|aw;ifPCOhl#za))%Fv13^| z*pFq@MP6{}EP>0?wI5*rB%8w8tM(hKW8`|Wu!StlPsXx-RbGKW{~eW#yngK)j_Rt= zx!y1CF)!m3Va~zxB{|Lqga_sRHH-7S-}5LMAiX!U1L93vx|PnRce^aEyf$;1ZvM$d zVh0yO))NBUja({|UJwM#iaQlftkw1I>ivE@N_`oqO->e$wpu!~n|8m2!}EHO=+lIv zlI8lx?Eq?JMq|2NsqI|W$fyZ~JC2SDqPmpuS*B(CGXZ~hWEB{(Br*q*t& zXa~WK6>avEmHvHXGxAMcLUxM(DFB`c=6nP)mTS5b3+igrT&z&Jsql5UrSbAijyG*G zx>qwUVzHqJXWQbY6+v#Y8B6->Q@fpCvVO#Z00M(8LMvrEpHM4lU7Bn*l2Oa6|L^Ug z^^*g_^05K!z7EA|+3St?%sgFor@>jKa&loD;ugv(%b}7ZH%k8nCvzn?hT9>#u9$F? zgk(NEqV~r# zGX$Wa(7l||Z`+1ittw1HIKoNC+L~_O2jYABKYZRHrHZ|{FN{BAU%U94ZgM@l2!|1K zWw>P3=q%AP{bvPS*muAk)>eO?aU}Tv;Ncv0IDone)JZ*`KP5)dR{w#~f&LNC9P;{0 zr0I~b1%+i(A0qYw7-eB~W;lKd&@*-fF5E}bO?s%?HFihULVr$7b%n5h}KHSKouQ@XTmp#K<;^xj5ey;sjZDFtT$OZHF$KxU)0RzsYzV zZc~o{e*Z?f9uf`NAbbl6$W8Af(ahoutBC$nCk8|Tl9Ll%xQKRGikXvq4h@iAArLVZ zCPu1fOq7E<*ptx%*ry!#C5!J*-DtdTcoiiWT+02L7Iv$Bw`)uI%_UaONX6gtXUas-& zCr662^|w6*7Ip=21)m=lxgiZN+}Epf<~nevm?;s2__^iE0)Udcr+gS(6~Z+nhDo#X zuuCGc97-5u{Q&6+seUg5P-t0T*>Tv>!6xjeUGZpc6p@K@1vlX(i0S?+@3woGkcE@< zZ{(?>@n0vL+U4csEfhGN3r_+TmiBHLN#Mc6YTQVWG-^y}Okp`QrXTW(nez$EX=VZW z0!tvu9#y)XV!@~RvpFQ`0m7oD`NtG1Zh)u4wPbi|P+8)oeT3&MEJ%M#fUTT2a&6CD z#e5rJy;w|&ptb<0W)LnB@=WG6e~X+pf$QE~RU*h8T7YW;@HiJ$_{g}|tcE|uxQa7* z6U59|5E=+=QY zqUK(UE75K=q6ydBVloLYKBl&rT{=16T$6=*KA%*{U#gKdxRH;c3N6<5kDtHW-WYij znK*5#)>c1c|54tYB#`V>Dn9Tc8`SRP45S&yOb}a(bJKrpWS^HKX2v#bI2clnK1=nsuF(L{=_=awHrbr8Q!nXOQUYboTH4aoV}kKIxA zjq!e3GpqCzLAIs{3t7pxyZdzfene4o2Yoj=N4kChJeHg(r~w|c#Tr&nMlRyCT@I8r z>^a~+lfuFGq%a!2X4*$@UE>~&%Zj z_dB@5+#{-F9Q|CHzPqRe_2y7e87bD(+~Y;)I97jy#dhAQo&RZv)l>@M0}K6l!hP0I zRJ(Ql#YD|4$tTchK5A@{IR3|*f?q;wzuz?7LkL_Eqs9h^Dw+kn=}hF@`=nin2N^sH zew97#cieaN87E9*c8*v%P$3}Zl46zOgr@G>cm73N7{K+jt-nJ{;rm@{UYpO$KUy_@yy?mGlm?Gx`AH* zq%yQ>*&2P}E9H04cJv~>;)Szyx9!culbf2g=mXt4b)?{U)WuC2CDPb-^=3S27nh>% zMOo3wdi8R6EF+-$<-ZGS+ibpvQ&u^L3*u&rCeJ7Ku}*Dirz{fIZ8i;*K|XpsEs*(A zc*gQx{Q@?*?I4}qV1ORzVBiE~&ZPs1Ko zc%Un|T2crv7pUyiE+4m^o3mtGhaW1r5`q~{t53=DrRB+uO5dRpE(t%PxeH9>s|(5k|Ww&PYfNrKI;*$?2I+j!^q68DOonISyNPR z2fDUK6jk}pk9!?f6>t^-4*f^zpv(rNrD*&}Pk`NxoVLh6EZTVWyn0T73(2CjY_PMr($K=`6*;(u&iL?cnZZW_vb!?DY94yLJrV@pg zlPo|E*;p?VBG^P*RsAEEy30+!g{uZbCXccIeAxa ze=DITf6u3v$Od~*j3QIX_knRoy_2qg-D^v@%(jXytxg!NfJ&bK(wgY&Imn*VVP)abfon?MbEWLE5vwBJKh)q-;cMLON+1u>e?ebVv?R0uiu2a z0iAbMjelfi#3r*eC9>|gNOq9NXZX-n#o_!*ov}ICY{dp(zRH*0ht4~_-q-AKeY+r2 zQB-qo)~SV7M!5D>rS}dRDr=E1ugplgI8m=!E){&PUhVLGI0UL~Z)+voq=9Rt6`~Tf zuY*zr_kynq=(Y?}32w#^HVbdk=agv6Td78~Vk(HinW2g;u9F0iQJnO`;ZAK1{x?J4bC5L=p?Xv{~-Hh{;_c*+o-;~}}ZseOHuwi@R?xop{{U)Y`A)O{^<;mwk|Lhp%TWhrTT3mQO|Rf+1(~gkQ>m zMhW*@1g&ASsoSrZZ@oRNalE~HLRPri4o+m&hg`1GOP|A8orN?jfaLh(+hml{V`8*x zkE|K892&f4^CffQAmxvT<-+?+w9<78SgqX;q`X`2^dy+-`OpEp} zXMbBUD%jO5^BhuaZyEM{yUaQNcuqN-o1O{F9l|t|MkaJ+GQG(0J0xA0C3~V!C=04k zhAVh68ewjH&0c?ZcIT_S8LP2hRd@=n3LJi0`Q!+7^JL~{h0om5XLp^cev!yYRdU5c z!qMR`3kH~D=CF!3eMxzBSazj}@YjlB9Hxsmq`Q%HX$L^k$ych~38gysr0WkjCwO^D zkhw3bKc3gtd6L%oOAoYjk~phy{6RBB;Xi~UJH02Lgh9c3Xl7`ApB6>OOgzI(~#+@m} zwgkM-&@(NzTdB5A7x6AgqT_dbMZQxiH%p;C^R_@-xY5LsE`;|VVYytQBbjh3zZJ`_ zRweTpD`D8=l<6y}PdK!pIYa&{|?t#xgYm8w4BEpeB(JPpusx ze_dUd6ZBnW%HUPBV%yY_x~-jVf95%aq2`z+(Vs1;x9pDT=K`}t(*nj%#RuD}&3hoe zZKsk=fibW&hv+k0xS6&mGnd#fjs1!Ft)5h%Yg)qf*Yw*&H(zQWF>`+XXWK=3T=w!S18_&$oS@IVrtB*~yTMyuw$8r{`g*K&Q`k8FQEIjE~M5 zWjRFIJ9!crZ;*O^uh^ndJbxqIYJ=i9>epV+U5!RG$=EL#HrfqIC14)Rsc%O-)@g>8 zoZ*AZ6hZ#g?YO423T=EU%G7kD6FU=z)|y7an_$54^rmSimG`uYPl{eRl#8By2v5y|g4*z3DeJ3Fl#@HbnfL%nIvEd5c_OrSUyTg!1ChO%w76$HdsWVuiY0HxGM-Y2d#-5VLsVtgxM@2kf73m);9%fzP- zHc$GLA}KuX^bQ)eY{c(Nq_gSFBn?-b?k7@u3$QDT)z+i_t)A|#$X^N`!^vfQ9u!su z@vozM9f^aK`^JPv*!!=lxQ;42KOWKJ;H?!G|(2*OA;I=eEr(J3VnyH z+;RpZZSJ?rH#z&!giQD>ynCt9b>Dju&nYOA`UT(WIqNdF2kYu12*FCRsYVUTt0yM@W^#r2u6J&lUdd-<9jUHyzLXYJ$y^P6m2J_PU}o}YvlE2=P&=yIoD~68NEXH~h9j)6nE6T8(mO zp~DabhdPb$Kvt@X@9358_#5+6i3b!PtN#A#Gpzf) zK1(gB>kxmdYI>(EUM}iL{g5t^yc(%Hu~tmUirJ3215bxSEV4~d$#@W|dZ1C{D|c&d5QM5YJ~5W0wn`SC zTz6JBO2*Ps&-AZ~2MGaZ3n@@rhX}5b_jl$B(HGSmH>L`YC!yCM7}3D_?k+1p#?#T@R%7XWT!rU@!Zb-fz~1$&s%ct7 zGq5p|pn2>r0C=t?E7Av?m$Q89R-N;gaka~trRue9+3jk+l!LO;eKm)03RM<7VqNnF z^W@nX-0~3284MR6@y9_b^7f>8&ViO@LN{m4c|+q-D~y@*yHph{5yN?`0>rI6*mEDH z{*k)#W)O_lSAY$sS@~bvXC2C%V7aW7J8~HSCjyELrX;S))}bz282|U8Kj3Jud93Pu zgQ0?QX7e!OY6SlqH{08ePYOcL@(D*@laE`4fdhixDc z|ISt{p_-J=yb}dP$+mC&?zuk|mk=w*d=Pt!xX}}P>fb~KDb;ZN=S)s}c;m@-_2@6E zrDZE!55?QVFx8~X8HgL97dG1~3 zi&#F{lyhgDRv3uYT)pJsSpI0}+lHv@$7q25`JdG~L6*+zV*zJkFnS%`MHUe6TLytVJV_Oz^}PxYKQ2u>pZZ zM#SYX2GYe5K7*Esb)#4p>8o&Gd zR1b)Sr-mly9k(FBpvBAYRBEaSO4U~>(PpFdIk3O&qPeq`HNb3Evas0o`BBCP?nv+T z%6b?r1($H1?2(hcgm|0}-rUi_=2*dL2+3L9zNUEJ!D6{zc^?r6(-TrxYAkzlh1fLf z>7w+LC4>P0IX#ktubIE%Kx7~rRxItt!ap~|1 zS8OUE1$!!X1~piR;ZLZlFn>+JZgdh_?Mn1k4Jq^zU8);zr^S?B|4pc}8k>E>eB3XS zSPd3Mxsz>t*f!QagaXO=S0My8$)qawtdn!t8L&|A*s%$@Anm%p22HEOGSUkq-8AN8 z#V%*lmws*@!e@T))bT=fw+|c-!20^rj*T6CEwZm2J$aTxYgCOT)5MjCirQuHg78)j zQzCkYu7YMZYUoXYH3}OlwbT}i3$)4i3MX1SWgOTbnB953)13{B3a|DY%eBJe)~#WQ zzqx$Z^6dtyp(I^*%9mky8^AH{7rdH*w(5O$ww~K!`B0A2|9G)ga-&>{|G_!Wqyy1T z(^+szr*SbQo{Bzv*9=#eO#`U@L%jdtSABek0)ebt#DwA%w9z)X+uShi!$ABHRVu=K zY9fgiWzwm1t5kXk#d}S^`um50wlllBc#_PqKMG0c+xQ8yQ%;hQz!lQW<5Fg|INbm% zrb2Z_Bb0fmn7)M@atj?eEaKi=iQa;Xn3sHmhEp?Z=w*c?YbwfL7chGF>6fYAs*7Fl zrA8z(dC3+O9V)`S#^(a2resv4qclf99ViZc^zs>_v~A0rBgjP5R}Y+%0z9 zqMN~(MXikf<|g1~JIe$~|KQ{}5oW#0RYIhO^E$iP>r^(avs{OZzP}MOf9_B&7VM+l zMi%+gr}d(_o_hW`SH9%$l>-}F%R|y!LJ9xClU&rQ2xk#z`*4^g+mtT00=XzOO^O;=*g ztJgAdM@q@zw5~@0eU<);Enw<*y;y)YW{H zI+Uh4p-Zf6z|~yQso?>VjkmV$U%v67%gkQ(2(P&bNt8y+sMKz*{`t!^MC%mrV4W3rZ!p31R3=JkG)T(GXi>f}kW%xnOSfQg*4#XL{dASS zoP~ar6(8IfBa*1zOOAds7XBfec)6wRV!3^W#~J<|3zi->uNkaUb?B4%sG&+Ef@c;#P#!fT_&no!a8=Tt+oh@&Ku?G(|PL9b9jra~kk4jrZ2 zym9PyE>c@YKsx z`fEkUbh9!)8pP`wmYl9>Ho+cl6XB%ZLW;F|Vy~6=gl%Fmnl-&{GHk^xldaCtz>SYp z=O&jumKtcDo+mUMBCSzq(Q5vr5-RzGVvA8TO_?5|aF!`!VH5jkVqD0=RUJ<^K^m!0 zy)bb4%^|6p{>aUFZ?bfBSuK1s7Q;y4REtP>ykTTK!fZWhFj|uHH`FmEVg-CAJtvg2 z{Db6qQ+$>9ZI0pq()78X<}h)MxeT8<;U2K?#&W14e4rm;jFJ}^n2s;Vz;}FfRI%rN ze`GjUAet~P_q4a1#@i`nB?Ud8kM;6Q?yfI+M?N5n z8FH^a+8vPNm%fnNX$d^4A31fI3}cfHoF}#Cfv#XHZz8UI_K7+@4=tCS-z`dpVlyps zA`>e&5{fqa_Fh40>U-JkW-1gF2+2h$hV$dPVWR%@=*^(rRjoT1z740Gho4JweNko= z{{6ZCEV&R9vLZH_P0qjKSz-N4pP2A@_LXk z!=r+*=Y$oG7%9bKF7Ks*o{|!1C5|OeP0Ttnz_O7ftax@DlK2>>nf-EjrpLVFr)dU5 ziC;l_at0h2Y=2dZ9C0R_wUYHpjcz3lvbyG1Q0{rkqVJEd zq!NoI6+sHy9bqlvGS=!bi3h{#Ho7-j6?dX?v25@-zd|^Q7BlErtFjMB#7JyG%{{27 zU~XPVU`R2`kj0NS>FgnG`)_IS2LpJYD_!l%oVeF#*T4M|df|cN(4=?jFtW7t(foIO z3Hdm5p%WRQ@+bx@UPCSqD3(gteQ1iqNY&0Lx7aV^$Y(p15#vW~wZA3fE9ens>m2bo znNnx?tj82?xM!S}sIE;?e%!r(7thcm)d0}LSD5;qr z{W6e)L-Yz4u)vn*K&39ldvybZ(pi+sjwG&l=0#;=GtgFgHSpmT3=r9MKUpDJT{B$E zft1R=y~L7LUeI7j9N?#_RJxW=;9RVr%Y6f;sCD#0J*L%?WrbZX1fnKA z&=)E~%NShpbG8f~>LxERSDssE~9<9)oxda)%I@4>S|G7OdXoD zrqLcw3540l!rJZNV|JPL@o+b1<@<>1&XK^v%bSrKEzR{6u4YbDFIsq>?2L$ni}`C> zvjVVssb!I2Q##qLoW*EGRV?X{H4`%tBUUc1(b~R;%@TV_$ay%eDk&3d`iTFH86s%XSF(=Y7Oi=9w|V!Cr3J+05!Wg7%Y0VMlyvyn)cCd@n{K z$N6yIf)EEZ6q9;TaKRkWH!Iz*M+!zTmC|IYPkElU&w?IMATJ^jJ;} zdcj}&CdX<$W^5%4mc@z0fQ%-)cMT{V*+wGsqP{s$HrY&`W1aHd;L5pp8)&Mh^NrS8 zt#c#IvUcUjbBeVYb0$-5xXG9DX2Q!X>82QpA!Ys9=7f33`G<>YEy9u^x2!D(w3_P{ z2y$b-K%Nz$$;r$jo7I(0I(C^>VPG}Xo$a}F{F=x(ZIJy$P9vAs7QWS48K7@G98a7nC z?PMmfY0UJpHcb8h+A($`Jbn$o+6K)rpV&cI)k8;Uc?)}A)wGs}6SLKK1wFop7FdXy=1cy`F zR}@fMy&IOxa}n2q=e}SNjl@gU+zNriPHH`ZBG1ZelFNWBS4E^KqWIyyD3gK1tV3|! z_8ccLBMfArp? z@~kL}P_EIZh?%!On@N2+M&HwPq;Xv>KKrA#^mEs$aJPAR=7p2_$cW{+|9c?gjY+YJ zI8tZV6UnkJ<_kkK^NBsX%KnU-@p4yo+=o$H=wSI=*er)4sh06I=C=UES5YbwG$5E0 z_z3-J`^z}MORo2|uHTl8H9HNsqT%=WJ*V+0v_dfUBbD!s& z@8`>lFBbyS-*+p|J232z*VMwflm{ZOmi->Tqj)8)%V6xJm5+pn{(mT+0K)Ju)jBa4 ztJXdUMVZuM?XcpeSexevb2lohASP@+jhL&gk?HS)5HQFWqZtZ_PM4!8yUXi|>_m-n zW#;&^hEoA8DbOhXjfa?8n&b<~q{FzqWUDX>-|axI#XAF5$259rm8H=c66-`i`s0pv zN}q*|csxX!`IU*)<@>y58hq)qD=we)LqK^h8`fiXjL2Bm^77xpffE71CC@D5exU=# zX#tg)4-s-{VZ}{idY8YyGFW#@irz|dM{OsEtJi)n+32*4?xYe9HJPm#pY_cS+LHB{ zcT=79*2C2xL=4USDIjKBbla;U8=a<2m0M1rG5*f@ogv!^WQOf9okk|~-SGv|j4JY& z&%qgTBjG2qg_Em80w8#%`?Eis0)OyDqrIqVaqaIbcl4}{m&`qZ3HkCl(QQH#-xLU4k#NvOHq~QwCP-3ndD@b z;eI zbOpT8i}-suEIGR^p)mWw~lq>ED- zwVJ)1?L3w8Wa0P0yZ=g!Dc#!9uFVb zrU%}-#gvNP7CsZgQYDRk*7L->LhD}@(ZAZ2NILQr${HHBFLnCbNhFD-g)q|f>7vioR1r@ufqmO=xCd#p{9ruoMtL{yd7 z@42{&H)#_|N233v*L4V9+yb5aa<}*$w&NDV#h=Ud=stakVoElTaPH7Khlusk_B?{={phO8*G@K? zs=;lQyls58-1S_|A+ZI@5ovL&%_J5Qj@E*XT!yOc^T_@<-&y>V${d<*m@j-ZoJ=n= z!O|qe_mAecLwWyH2Z3*z?p9IpAI89m$QwsY?gP3Mal?gAcXk*{5l^Z0eaeqc%aFpe z8HJ*(n7eSFvWrpni}aY7;-i56yA}ancXBktY+A<|tqT*^>O<-@oC5bN>0iWTeYb-# zW>=fIiTCgBMs*AsTh#T9fWF(_Cj3d_4KIa+z)$PZNixfYWg7)Y47p+K@h3rVUHK>) zez3L{=-13mBhZWN`f{FOQ{}V#e6{QOx7e-}MTrRSqB!vz1k_H!%H;tuFaREl`F+tSYEQ+%`lLtHT$EpI+lhwZkVOLUpqSZh5#BV zh7y}~3`s7)5i^qGFTQwt-()z~7%;2~-XJ%!I*Isdh3 zFy5L7r$Wsk0oWga^;N=KTGNAe?PRvQ#T-8sv)xfwNe&A;r=2NtnQ6AQqei6Rkb&^R zPcF_54@2$k`D;vDKX+_is2lG6mA(gQAWY6c{8G^P8DI?>3axHL2Y=k%s86^#8JCn` zOsH8c26a(VQUZu7$4rb!PuceA>?VKX`e6&+ z$456PbHB0qO=j;6DqqV^58hwaY#g6qyuNzlbvyJ$P^g)EHEDIIUyY-=as;(viDlhC z_oijIYf~xe1)w@g4Z*~l{Cq+9=O>EB4~bs0KfgqBYycEEUbO!BC@?ph0gSm4ijJK0rjEttZ{0H@)F`%pFnsJL`SlMqg-Gs6^;=_f z)2@Y&cR*iIJDb&U`7XR3Pteot4jvleeJMxWG3d^azWA?Tw#--kDd8NwI$v3S++uq; z)x}wWRUhnsdGoGE8>dFLv?mtQ_h1aQ9eGcW$9Pp;50<+=}L!0rl0AE(B5HTe>7 zlzNYS-}iqj>&P+%w9PigY@ZB27Y$D^Il>u`Up2aGlc%9%+(WHyuy}$wmKTY&VVYC) zal`QOt%WY4KM5NGuR{b#>?{e1pgj*kvr1}!bo`OVQW0nVco0PTys$I;Uj0upj)yz( z4d@9D)Lg%LhmT6}6b2&2FvMP6pPcd(-z$!qGAQG5Bm}Gct#Ob8vMZ4Ge)} zVq&n%3HBP4e9j3lk&o1xHk}-7Hc@F=Qt#+EWL5Zv)cd5r)#8?<0gr*iXe`N_l5;P5 zU6WpE7XcuX0KWycYJa6r6xj(z`sOnG6Eggq?fAubW#z@ks=Xa#FBla5C+@VmmWD|6 z&t&!9-z30Iv0OJYLz7rn?`vDy4 zy6DGlyj18-`~@~R*jDXH)1$MXL>_e=v@X9nqbtV2J93u&>K7~oC1>LWx`OPv=ilIB_Z11TYIu+Y$2)kTkmPOLN0v)nQL zVZg3_;3@z#=XNK1fTaTdo=T)P3#JB73lvU$|cvc@tyM}eytd9-ha5+hjM@sY~^yXOM+osD$$uWuLP^UXP%O2r3>RS<*T zGR108zObGvjrCDeKs5*Gba4v9i~85pY4y~&ouIed9JmjxoaC3p2$+JPX4OpfOFJ!nz0tvfv%`4O_qn*ju%XDR0|rHozoll0ne%e zpK`m~Z$Dkj({`WF8^4~y%slp1v{>q%yK{boGwROOp^AW_uYbu5$#u5|zN&Raa5;mu=u6k{c~*7Ok;`c{ zE0xUhY}B}lK&iOJ6%%(N>@JUv?umgasV24A#w-VyLMde3F6e!RmI`j%`D`prJKTns zGksuvd!|An@90tiM*m^yR%2PC3$beA0bWAwds8Qqc From 11cc634c0821da2521cfc6013786c675da9b0701 Mon Sep 17 00:00:00 2001 From: Suzanne Daniels Date: Thu, 7 Oct 2021 17:27:56 +0200 Subject: [PATCH 010/245] Fixing typo in contributors Signed-off-by: Suzanne Daniels --- docs/getting-started/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index ab217d7655..12662ddfeb 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -15,7 +15,7 @@ system using tools like apt-get, npm, yarn, curl. Docker knowledge is also helpful for making the best use of your Backstage installation. If you are planning to contribute plugins or the project in general, we advise -you to use the [Getting Started for Contributers](https://backstage.io/docs/getting-started/running-backstage-locally) guide to +you to use the [Getting Started for Contributors](https://backstage.io/docs/getting-started/running-backstage-locally) guide to do a repository-based installation. ### Prerequisites From e27844cd073d58dedd444dcc0863c462874030e7 Mon Sep 17 00:00:00 2001 From: Suzanne Daniels Date: Thu, 7 Oct 2021 18:14:29 +0200 Subject: [PATCH 011/245] Prettierified index.md Signed-off-by: Suzanne Daniels --- docs/getting-started/index.md | 49 +++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 12662ddfeb..7df10f8bb6 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -4,8 +4,8 @@ title: Getting Started description: Documentation on How to get started with Backstage --- -For most Backstage installations, installing the standalone app will bring -you the best and most streamlined experience. In this guide you will: +For most Backstage installations, installing the standalone app will bring you +the best and most streamlined experience. In this guide you will: - Deploy Backstage Standalone with npm packages - Run Backstage Standalone with a SQLite in-memory database and demo content @@ -15,15 +15,17 @@ system using tools like apt-get, npm, yarn, curl. Docker knowledge is also helpful for making the best use of your Backstage installation. If you are planning to contribute plugins or the project in general, we advise -you to use the [Getting Started for Contributors](https://backstage.io/docs/getting-started/running-backstage-locally) guide to -do a repository-based installation. +you to use the +[Getting Started for Contributors](https://backstage.io/docs/getting-started/running-backstage-locally) +guide to do a repository-based installation. ### Prerequisites -- Access to a linux based operating system -- An account with elevated rights (eg. via sudo) +- Access to a Linux based operating system +- An account with elevated rights - `curl` or `wget` installed -- Node.js Active LTS Release installed (currently v14) using one of these methods: +- Node.js Active LTS Release installed (currently v14) using one of these + methods: - Using `nvm` (recommended) - [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script) - [Install and change Node version with nvm](https://nodejs.org/en/download/package-manager/#nvm) @@ -38,9 +40,10 @@ do a repository-based installation. ### Create your Backstage App -To install the Backstage Standalone app, we make use of `npx`, a tool to run Node executables -straight from the registry. Running the command below will install Backstage. The wizard will -create a subdirectory inside your current working directory. +To install the Backstage Standalone app, we make use of `npx`, a tool to run +Node executables straight from the registry. Running the command below will +install Backstage. The wizard will create a subdirectory inside your current +working directory. ```bash npx @backstage/create-app @@ -49,7 +52,8 @@ npx @backstage/create-app The wizard will ask you - The name of the app, which will also be the name of the directory -- The database type to use for the backend. For this guide, you'll be using the SQLite option. +- The database type to use for the backend. For this guide, you'll be using the + SQLite option.

Screenshot of the wizard asking for a name for the app, and a selection menu for the database. @@ -57,7 +61,9 @@ The wizard will ask you ### Run the Backstage app -When the installation is complete you can go to the application directory and start the app. The `yarn dev` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window. +When the installation is complete you can go to the application directory and +start the app. The `yarn dev` command will run both the frontend and backend as +separate processes (named `[0]` and `[1]`) in the same window. ```bash cd my-backstage-app @@ -65,14 +71,16 @@ yarn dev ```

- Screenshot of the command output, with the message webpack compiled successfully. + Screenshot of the command output, with the message web pack compiled successfully.

-It might take a little while, but as soon as the message `[0] webpack compiled successfully` appears, you can open a browser and directly navigate to -your freshly installed Backstage portal at `http://localhost:3000`. You can start exploring the demo immediately. +It might take a little while, but as soon as the message +`[0] webpack compiled successfully` appears, you can open a browser and directly +navigate to your freshly installed Backstage portal at `http://localhost:3000`. +You can start exploring the demo immediately.

- Screenshot of the Backstage portal homescreen. + Screenshot of the Backstage portal.

Congratulations! That should be it. Let us know how it went: @@ -80,10 +88,13 @@ Congratulations! That should be it. Let us know how it went: [feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) or [plugin suggestions](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), -or [bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md) -you have, and feel free to [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! +or +[bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md) +you have, and feel free to +[contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! -The most common next steps are to configure Backstage, add a plugin and moving to a more persistend database: +The most common next steps are to configure Backstage, add a plugin and moving +to a more persistent database: - [Setting up Authentication](https://github.com/backstage/backstage/tree/master/plugins/auth-backend#github) - [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres) From c80e7ffbf93a7d4f3dcf7a617556f25a0d1e4e62 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 7 Oct 2021 12:34:12 -0400 Subject: [PATCH 012/245] use relative temporary file paths in resolving app destination paths Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index bc33fd91ae..68aad6e2f4 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -20,7 +20,7 @@ import chalk from 'chalk'; import { Command } from 'commander'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, relative as relativePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; @@ -94,12 +94,22 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - const tempFiles = await recursive(tempDir).catch(error => { - throw new Error(`Failed to read temporary directory: ${error.message}`); - }); + // Get the temporary files relative path in relation to the `tempDir` so + // that it can be used in resolving destination file path + const relativeTempFiles = await recursive(tempDir) + .then(files => { + return files.map(file => { + return relativePath(tempDir, file); + }); + }) + .catch(error => { + throw new Error(`Failed to read temporary directory: ${error.message}`); + }); + + for (const relativeTempFile of relativeTempFiles) { + const tempFile = resolvePath(tempDir, relativeTempFile); + const destFile = resolvePath(destination, relativeTempFile); - for (const tempFile of tempFiles) { - const destFile = tempFile.replace(tempDir, destination); await fs.copy(tempFile, destFile).catch(error => { throw new Error( `Failed to move file from ${tempFile} to ${destFile}: ${error.message}`, From c120e220dd726466829723a073b74def8ae91f90 Mon Sep 17 00:00:00 2001 From: Victor Perera Date: Fri, 8 Oct 2021 17:31:07 -0500 Subject: [PATCH 013/245] EntityFilterType hidden and settled Signed-off-by: Victor Perera --- .../app/.changeset/spotty-mayflies-refuse.md | 5 +++++ .../EntityTypePicker/EntityTypePicker.tsx | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 packages/app/.changeset/spotty-mayflies-refuse.md diff --git a/packages/app/.changeset/spotty-mayflies-refuse.md b/packages/app/.changeset/spotty-mayflies-refuse.md new file mode 100644 index 0000000000..8f98365677 --- /dev/null +++ b/packages/app/.changeset/spotty-mayflies-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +EntityTypePicker can be hidden and initially settled like EntityKindPicker diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index bc30bf5b17..799598b5f7 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,7 +22,15 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -export const EntityTypePicker = () => { +type EntityTypeFilterProps = { + initialFilter?: string; + hidden?: boolean; +}; + +export const EntityTypePicker = ({ + hidden, + initialFilter, +}: EntityTypeFilterProps) => { const alertApi = useApi(alertApiRef); const { error, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter(); @@ -34,7 +42,10 @@ export const EntityTypePicker = () => { severity: 'error', }); } - }, [error, alertApi]); + if (initialFilter) { + setSelectedTypes([initialFilter]); + } + }, [error, alertApi, initialFilter, setSelectedTypes]); if (availableTypes.length === 0 || error) return null; @@ -46,7 +57,7 @@ export const EntityTypePicker = () => { })), ]; - return ( + return !hidden ? ( , ) => TemplateAction; +// @public +export class DatabaseTaskStore implements TaskStore { + constructor(db: Knex); + // (undocumented) + claimTask(): Promise; + // (undocumented) + completeTask({ + taskId, + status, + eventBody, + }: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise; + // (undocumented) + static create(knex: Knex): Promise; + // (undocumented) + createTask( + spec: TaskSpec, + secrets?: TaskSecrets, + ): Promise<{ + taskId: string; + }>; + // (undocumented) + emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + heartbeatTask(taskId: string): Promise; + // Warning: (ae-forgotten-export) The symbol "TaskStoreGetEventsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listEvents({ taskId, after }: TaskStoreGetEventsOptions): Promise<{ + events: DbTaskEventRow[]; + }>; + // (undocumented) + listStaleTasks({ timeoutS }: { timeoutS: number }): Promise<{ + tasks: { + taskId: string; + }[]; + }>; +} + +// @public +export type DbTaskRow = { + id: string; + spec: TaskSpec; + status: Status; + createdAt: string; + lastHeartbeatAt?: string; + secrets?: TaskSecrets; +}; + +// Warning: (ae-forgotten-export) The symbol "WorkflowRunner" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DefaultWorkflowRunner" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class DefaultWorkflowRunner implements WorkflowRunner { + // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts + constructor(options: Options_3); + // Warning: (ae-forgotten-export) The symbol "WorkflowResponse" needs to be exported by the entry point index.d.ts + // + // (undocumented) + execute(task: Task): Promise; +} + +// @public +export type DispatchResult = { + taskId: string; +}; + // Warning: (ae-missing-release-tag) "fetchContents" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -206,6 +280,14 @@ export function fetchContents({ outputPath: string; }): Promise; +// @public +export class LegacyWorkflowRunner implements WorkflowRunner { + // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts + constructor(options: Options_2); + // (undocumented) + execute(task: Task): Promise; +} + // Warning: (ae-missing-release-tag) "OctokitProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -216,6 +298,15 @@ export class OctokitProvider { getOctokit(repoUrl: string): Promise; } +// @public +export type RawDbTaskEventRow = { + id: number; + task_id: string; + body: string; + event_type: TaskEventType; + created_at: string; +}; + // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -235,6 +326,8 @@ export interface RouterOptions { // (undocumented) reader: UrlReader; // (undocumented) + taskBroker?: TaskBroker; + // (undocumented) taskWorkers?: number; } @@ -260,6 +353,218 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { validateEntityKind(entity: Entity): Promise; } +// @public +export type Status = + | 'open' + | 'processing' + | 'failed' + | 'cancelled' + | 'completed'; + +// @public +export class StorageTaskBroker implements TaskBroker { + constructor(storage: TaskStore, logger: Logger_2); + // (undocumented) + claim(): Promise; + // (undocumented) + dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; + // (undocumented) + get(taskId: string): Promise; + // (undocumented) + protected readonly logger: Logger_2; + // (undocumented) + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: ( + error: Error | undefined, + result: { + events: DbTaskEventRow[]; + }, + ) => void, + ): () => void; + // (undocumented) + protected readonly storage: TaskStore; + // (undocumented) + vacuumTasks(timeoutS: { timeoutS: number }): Promise; +} + +// @public +export interface Task { + // Warning: (ae-forgotten-export) The symbol "CompletedTaskState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + complete(result: CompletedTaskState, metadata?: JsonValue): Promise; + // (undocumented) + done: boolean; + // (undocumented) + emitLog(message: string, metadata?: JsonValue): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; +} + +// Warning: (ae-missing-release-tag) "TaskAgent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class TaskAgent implements Task { + // (undocumented) + complete(result: CompletedTaskState, metadata?: JsonObject): Promise; + // Warning: (ae-forgotten-export) The symbol "TaskState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static create( + state: TaskState, + storage: TaskStore, + logger: Logger_2, + ): TaskAgent; + // (undocumented) + get done(): boolean; + // (undocumented) + emitLog(message: string, metadata?: JsonObject): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + get secrets(): TaskSecrets | undefined; + // (undocumented) + get spec(): TaskSpec; +} + +// @public +export interface TaskBroker { + // (undocumented) + claim(): Promise; + // (undocumented) + dispatch( + spec: TaskSpec, + secretTaskSecretss?: TaskSecrets, + ): Promise; + // (undocumented) + get(taskId: string): Promise; + // (undocumented) + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: ( + error: Error | undefined, + result: { + events: DbTaskEventRow[]; + }, + ) => void, + ): () => void; + // (undocumented) + vacuumTasks(timeoutS: { timeoutS: number }): Promise; +} + +// @public +export type TaskEventType = 'completion' | 'log'; + +// @public +export type TaskSecrets = { + token: string | undefined; +}; + +// @public +export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; + +// @public +export interface TaskSpecV1beta2 { + // (undocumented) + apiVersion: 'backstage.io/v1beta2'; + // (undocumented) + baseUrl?: string; + // (undocumented) + output: { + [name: string]: string; + }; + // (undocumented) + steps: Array<{ + id: string; + name: string; + action: string; + input?: JsonObject; + if?: string | boolean; + }>; + // (undocumented) + values: JsonObject; +} + +// @public +export interface TaskSpecV1beta3 { + // (undocumented) + apiVersion: 'scaffolder.backstage.io/v1beta3'; + // (undocumented) + baseUrl?: string; + // (undocumented) + output: { + [name: string]: JsonValue; + }; + // (undocumented) + parameters: JsonObject; + // Warning: (ae-forgotten-export) The symbol "TaskStep" needs to be exported by the entry point index.d.ts + // + // (undocumented) + steps: TaskStep[]; +} + +// @public +export interface TaskStore { + // (undocumented) + claimTask(): Promise; + // (undocumented) + completeTask(options: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise; + // (undocumented) + createTask( + task: TaskSpec, + secrets?: TaskSecrets, + ): Promise<{ + taskId: string; + }>; + // (undocumented) + emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + heartbeatTask(taskId: string): Promise; + // (undocumented) + listEvents({ taskId, after }: TaskStoreGetEventsOptions): Promise<{ + events: DbTaskEventRow[]; + }>; + // (undocumented) + listStaleTasks(options: { timeoutS: number }): Promise<{ + tasks: { + taskId: string; + }[]; + }>; +} + +// @public +export type TaskStoreEmitOptions = { + taskId: string; + body: JsonObject; +}; + +// @public +export class TaskWorker { + // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts + constructor(options: Options); + // (undocumented) + runOneTask(task: Task): Promise; + // (undocumented) + start(): void; +} + // Warning: (ae-missing-release-tag) "TemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -286,4 +591,8 @@ export class TemplateActionRegistry { action: TemplateAction, ): void; } + +// Warnings were encountered during analysis: +// +// src/scaffolder/tasks/DatabaseTaskStore.d.ts:51:9 - (ae-forgotten-export) The symbol "DbTaskEventRow" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index 284e372b4c..cf2a554189 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -14,3 +14,27 @@ * limitations under the License. */ export * from './actions'; +export { + TaskAgent, + DatabaseTaskStore, + StorageTaskBroker, + TaskWorker, + LegacyWorkflowRunner, + DefaultWorkflowRunner, +} from './tasks'; +export type { + Task, + TaskBroker, + TaskSpec, + TaskSecrets, + DispatchResult, + TaskStore, + TaskStoreEmitOptions, + DbTaskRow, + TaskSpecV1beta2, + TaskSpecV1beta3, + Status, + TaskEventType, +} from './tasks/types'; + +export type { RawDbTaskEventRow } from './tasks/DatabaseTaskStore'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 09cba596b1..8f659cad02 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -46,6 +46,10 @@ export type RawDbTaskRow = { secrets?: string; }; +/** + * RawDbTaskEventRow + * @public + */ export type RawDbTaskEventRow = { id: number; task_id: string; @@ -54,6 +58,10 @@ export type RawDbTaskEventRow = { created_at: string; }; +/** + * DatabaseTaskStore + * @public + */ export class DatabaseTaskStore implements TaskStore { static async create(knex: Knex): Promise { await knex.migrate.latest({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts index 2ac309a1d4..7b2b437be2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts @@ -77,6 +77,10 @@ const createStepLogger = ({ task, step }: { task: Task; step: TaskStep }) => { return { taskLogger, streamLogger }; }; +/* + * DefaultWorkflowRunner + * @public + */ export class DefaultWorkflowRunner implements WorkflowRunner { private readonly nunjucks: nunjucks.Environment; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts index 97e3a4dec4..e71fe596cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts @@ -43,9 +43,13 @@ type Options = { const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => taskSpec.apiVersion === 'backstage.io/v1beta2'; + /** * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced * with the default workflow runner interface in the future so this entire thing can go bye bye. + * + * LegacyWorkflowRunner + * @public */ export class LegacyWorkflowRunner implements WorkflowRunner { private readonly handlebars: typeof Handlebars; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 867e5bb127..59f605702d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -27,6 +27,9 @@ import { DbTaskRow, } from './types'; +/* + * @public + */ export class TaskAgent implements Task { private isDone = false; @@ -117,10 +120,14 @@ function defer() { return { promise, resolve }; } +/** + * StorageTaskBroker + * @public + */ export class StorageTaskBroker implements TaskBroker { constructor( - private readonly storage: TaskStore, - private readonly logger: Logger, + protected readonly storage: TaskStore, + protected readonly logger: Logger, ) {} private deferredDispatch = defer(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 7122c2fc3e..1f6d9d3381 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -24,6 +24,10 @@ type Options = { }; }; +/** + * TaskWorker + * @public + */ export class TaskWorker { constructor(private readonly options: Options) {} start() { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index dd13264aed..c068e8d7e1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -15,5 +15,7 @@ */ export { DatabaseTaskStore } from './DatabaseTaskStore'; -export { StorageTaskBroker } from './StorageTaskBroker'; +export { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; +export { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; +export { DefaultWorkflowRunner } from './DefaultWorkflowRunner'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index cd93404165..63b409e7ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -16,6 +16,10 @@ import { JsonValue, JsonObject } from '@backstage/config'; +/** + * Status + * @public + */ export type Status = | 'open' | 'processing' @@ -25,6 +29,10 @@ export type Status = export type CompletedTaskState = 'failed' | 'completed'; +/** + * DbTaskRow + * @public + */ export type DbTaskRow = { id: string; spec: TaskSpec; @@ -34,7 +42,16 @@ export type DbTaskRow = { secrets?: TaskSecrets; }; +/** + * TaskEventType + * @public + */ export type TaskEventType = 'completion' | 'log'; + +/** + * DbTaskEventRow + * @public + */ export type DbTaskEventRow = { id: number; taskId: string; @@ -43,6 +60,10 @@ export type DbTaskEventRow = { createdAt: string; }; +/** + * TaskSpecV1beta2 + * @public + */ export interface TaskSpecV1beta2 { apiVersion: 'backstage.io/v1beta2'; baseUrl?: string; @@ -64,6 +85,11 @@ export interface TaskStep { input?: JsonObject; if?: string | boolean; } + +/** + * TaskSpecV1beta3 + * @public + */ export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; baseUrl?: string; @@ -72,16 +98,32 @@ export interface TaskSpecV1beta3 { output: { [name: string]: JsonValue }; } +/** + * TaskSpec + * @public + */ export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; +/** + * TaskSecrets + * @public + */ export type TaskSecrets = { token: string | undefined; }; +/** + * DispatchResult + * @public + */ export type DispatchResult = { taskId: string; }; +/** + * Task + * @public + */ export interface Task { spec: TaskSpec; secrets?: TaskSecrets; @@ -91,9 +133,16 @@ export interface Task { getWorkspaceName(): Promise; } +/** + * TaskBroker + * @public + */ export interface TaskBroker { claim(): Promise; - dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; + dispatch( + spec: TaskSpec, + secretTaskSecretss?: TaskSecrets, + ): Promise; vacuumTasks(timeoutS: { timeoutS: number }): Promise; observe( options: { @@ -105,8 +154,14 @@ export interface TaskBroker { result: { events: DbTaskEventRow[] }, ) => void, ): () => void; + + get(taskId: string): Promise; } +/** + * TaskStoreEmitOptions + * @public + */ export type TaskStoreEmitOptions = { taskId: string; body: JsonObject; @@ -117,6 +172,10 @@ export type TaskStoreGetEventsOptions = { after?: number | undefined; }; +/** + * TaskStore + * @public + */ export interface TaskStore { createTask( task: TaskSpec, @@ -142,6 +201,10 @@ export interface TaskStore { } export type WorkflowResponse = { output: { [key: string]: JsonValue } }; +/* + * WorkflowRunner + * @public + */ export interface WorkflowRunner { execute(task: Task): Promise; } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index f8ba99336f..5f855a6e88 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -25,7 +25,13 @@ import { StorageTaskBroker, TaskWorker, } from '../scaffolder/tasks'; -import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry'; +import { + TemplateActionRegistry, + TemplateAction, + createBuiltinActions, + TaskBroker, + TaskSpec, +} from '../scaffolder'; import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; import { ContainerRunner, @@ -38,11 +44,9 @@ import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateAction } from '../scaffolder/actions'; -import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; + import { LegacyWorkflowRunner } from '../scaffolder/tasks/LegacyWorkflowRunner'; import { DefaultWorkflowRunner } from '../scaffolder/tasks/DefaultWorkflowRunner'; -import { TaskSpec } from '../scaffolder/tasks/types'; export interface RouterOptions { logger: Logger; @@ -53,6 +57,7 @@ export interface RouterOptions { actions?: TemplateAction[]; taskWorkers?: number; containerRunner: ContainerRunner; + taskBroker?: TaskBroker; } function isSupportedTemplate( @@ -89,7 +94,8 @@ export async function createRouter( const databaseTaskStore = await DatabaseTaskStore.create( await database.getClient(), ); - const taskBroker = new StorageTaskBroker(databaseTaskStore, logger); + const taskBroker = + options.taskBroker || new StorageTaskBroker(databaseTaskStore, logger); const actionRegistry = new TemplateActionRegistry(); const legacyWorkflowRunner = new LegacyWorkflowRunner({ logger, From d8ea1edcdbead3460d1cbfbfc56d067b5b476c87 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 12 Oct 2021 16:11:29 +0200 Subject: [PATCH 023/245] Add support for auth.keyStore in application config Signed-off-by: Marcus Eide --- app-config.yaml | 9 +++++++++ plugins/auth-backend/config.d.ts | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/app-config.yaml b/app-config.yaml index c24ac1701b..60e2a2b9e0 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -292,6 +292,15 @@ scaffolder: visibility: public # or or 'private' auth: + ### Add auth.keyStore.provider to more granularly control how to store JWK data when running + # the auth-backend. + # + # keyStore: + # provider: firestore + # firestore: + # projectId: my-project + # path: my-sessions + environment: development ### Providing an auth.session.secret will enable session support in the auth-backend # session: diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 61bbe15d05..b15b0da628 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -31,6 +31,24 @@ export interface Config { secret?: string; }; + /** To control how to store JWK data in auth-backend */ + keyStore?: { + provider?: 'postgres' | 'memory' | 'firestore'; + firestore?: { + /** The Google Cloud Project ID */ + projectId?: string; + /** + * Local file containing the Service Account credentials. + * You can omit this value to automatically read from + * GOOGLE_APPLICATION_CREDENTIALS env which is useful for local + * development. + */ + keyFilename?: string; + /** The path to use for the collection. Defaults to 'sessions' */ + path?: string; + }; + }; + /** * The available auth-provider options and attributes */ From 3a62f7619a9a915dec5bfb22ce3164c3a1e947c8 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 12 Oct 2021 16:14:30 +0200 Subject: [PATCH 024/245] Add class to use Firestore as a key-store Signed-off-by: Marcus Eide --- .../src/identity/FirestoreKeyStore.test.ts | 152 ++++++++++++++++++ .../src/identity/FirestoreKeyStore.ts | 90 +++++++++++ yarn.lock | 52 +++++- 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts create mode 100644 plugins/auth-backend/src/identity/FirestoreKeyStore.ts diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts new file mode 100644 index 0000000000..03fcc15a77 --- /dev/null +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { Firestore } from '@google-cloud/firestore'; + +import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { AnyJWK } from './types'; + +const data = jest.fn().mockReturnValue('data'); +const toDate = jest.fn().mockReturnValue('date'); + +const firestoreMock = { + collection: jest.fn().mockReturnThis(), + delete: jest.fn(), + doc: jest.fn().mockReturnThis(), + get: jest.fn().mockReturnValue({ + docs: [{ data, createTime: { toDate } }], + }), + set: jest.fn(), +}; + +jest.mock('@google-cloud/firestore', () => ({ + Firestore: jest.fn().mockImplementation(() => firestoreMock), +})); + +describe('FirestoreKeyStore', () => { + const OLD_ENV = process.env; + const key = { + kid: '123', + use: 'sig', + kty: 'plain', + alg: 'Base64', + } as AnyJWK; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it('can create an instance without settings', () => { + const keyStore = FirestoreKeyStore.create(); + + expect(keyStore).toBeInstanceOf(FirestoreKeyStore); + }); + + it('can set the project id', async () => { + FirestoreKeyStore.create({ projectId: 'my-project' }); + + expect(Firestore).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: 'my-project', + }), + ); + }); + + it('can handle keyfile file', async () => { + FirestoreKeyStore.create({ keyFilename: 'keyFile.json' }); + + expect(Firestore).toHaveBeenCalledWith( + expect.objectContaining({ + keyFilename: 'keyFile.json', + }), + ); + }); + + it('can use default google credentials', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = 'cred.json'; + FirestoreKeyStore.create(); + + expect(Firestore).toHaveBeenCalledWith( + expect.objectContaining({ + keyFilename: 'cred.json', + }), + ); + }); + + it('can uses a default path', async () => { + const keyStore = FirestoreKeyStore.create(); + await keyStore.addKey(key); + + expect(firestoreMock.collection).toBeCalledWith('sessions'); + }); + + it('can set the path', async () => { + const keyStore = FirestoreKeyStore.create({ + path: 'my-path', + }); + await keyStore.addKey(key); + + expect(firestoreMock.collection).toBeCalledWith('my-path'); + }); + + it('can add keys', async () => { + const keyStore = FirestoreKeyStore.create(); + await keyStore.addKey(key); + + expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.doc).toBeCalledWith(key.kid); + expect(firestoreMock.set).toHaveBeenCalledWith({ + kid: key.kid, + key: JSON.stringify(key), + }); + }); + + it('can delete a single key', async () => { + const keyStore = FirestoreKeyStore.create(); + await keyStore.removeKeys(['123']); + + expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.doc).toBeCalledWith('123'); + expect(firestoreMock.delete).toBeCalledTimes(1); + }); + + it('can delete a multiple keys', async () => { + const keyStore = FirestoreKeyStore.create(); + await keyStore.removeKeys(['123', '456']); + + expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.doc).toBeCalledWith('123'); + expect(firestoreMock.doc).toBeCalledWith('456'); + expect(firestoreMock.delete).toBeCalledTimes(2); + }); + + it('can list keys', async () => { + const keyStore = FirestoreKeyStore.create(); + const items = await keyStore.listKeys(); + + expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.get).toBeCalledTimes(1); + expect(data).toBeCalledTimes(1); + expect(toDate).toBeCalledTimes(1); + expect(items).toMatchObject({ + items: [{ key: 'data', createdAt: 'date' }], + }); + }); +}); diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts new file mode 100644 index 0000000000..cf8c082540 --- /dev/null +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -0,0 +1,90 @@ +/* + * 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 { Firestore, Settings } from '@google-cloud/firestore'; + +import { AnyJWK, KeyStore, StoredKey } from './types'; + +type FirestoreSettings = Settings & { + path?: string; +}; + +export class FirestoreKeyStore implements KeyStore { + static create(settings?: FirestoreSettings): FirestoreKeyStore { + const { projectId, keyFilename, path } = settings ?? {}; + const database = new Firestore({ + projectId, + keyFilename: keyFilename ?? process.env.GOOGLE_APPLICATION_CREDENTIALS, + }); + + return new FirestoreKeyStore(database, path ?? 'sessions'); + } + + private constructor( + private readonly database: Firestore, + private readonly path: string, + ) {} + + async addKey(key: AnyJWK): Promise { + await this.database + .collection(this.path) + .doc(key.kid) + .set({ + kid: key.kid, + key: JSON.stringify(key), + }); + } + + async listKeys(): Promise<{ items: StoredKey[] }> { + const docs = await this.database.collection(this.path).get(); + return { + items: docs.docs.map(doc => ({ + key: doc.data() as AnyJWK, + createdAt: doc.createTime.toDate(), + })), + }; + } + + async removeKeys(kids: string[]): Promise { + // This is probably really slow, but it's done async in the background + for (const kid of kids) { + await this.database.collection(this.path).doc(kid).delete(); + } + + /** + * This could be achieved with batching but there's a couple of limitations with that: + * + * - A batched write can contain a maximum of 500 operations + * https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes + * + * - The "in" operator can combine a maximum of 10 equality clauses + * https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any + * + * Example: + * + * const batch = this.database.batch(); + * const docs = await this.database + * .collection(this.path) + * .where('kid', 'in', kids) + * .get(); + * docs.forEach(doc => { + * batch.delete(doc.ref); + * }); + * await batch.commit(); + * + */ + } +} diff --git a/yarn.lock b/yarn.lock index 9915fb6279..b110393471 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3000,6 +3000,16 @@ dependencies: google-gax "^2.12.0" +"@google-cloud/firestore@^4.15.1": + version "4.15.1" + resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz#ed764fc76823ce120e68fe8c27ef1edd0650cd93" + integrity sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA== + dependencies: + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^2.24.1" + protobufjs "^6.8.6" + "@google-cloud/paginator@^3.0.0": version "3.0.5" resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz#9d6b96c421a89bd560c1bc2c197c7611ef21db6c" @@ -15053,6 +15063,21 @@ google-auth-library@^7.0.0, google-auth-library@^7.0.2: jws "^4.0.0" lru-cache "^6.0.0" +google-auth-library@^7.6.1: + version "7.10.0" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.10.0.tgz#6ab852f8e1abbe425aec695ac6501f80bd5eba56" + integrity sha512-ICsqaU+lxMHVlDUzMrfVIEqnARw2AwBiZ/2KnNM6BcTf9Nott+Af87DTIzmlnW865p3REUP2MVL0xkPC3a61aQ== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + google-gax@^2.12.0: version "2.14.1" resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.14.1.tgz#74885c5d9f01db412917fc49bbf20c4884828d36" @@ -15071,6 +15096,25 @@ google-gax@^2.12.0: protobufjs "^6.10.2" retry-request "^4.0.0" +google-gax@^2.24.1: + version "2.27.0" + resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.27.0.tgz#78ba655ae7707cb92ba37c35932381b5970c1772" + integrity sha512-xcLCeNKCqNm/w0At7/vdZHV/zol/iRS+PSAZTu7i6xNGBra/kWI3cfn4M6ZLQXeUEGbTVLJ4zGm53TVc4lvbDA== + dependencies: + "@grpc/grpc-js" "~1.3.0" + "@grpc/proto-loader" "^0.6.1" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^7.6.1" + is-stream-ended "^0.1.4" + node-fetch "^2.6.1" + object-hash "^2.1.1" + proto3-json-serializer "^0.1.1" + protobufjs "6.11.2" + retry-request "^4.0.0" + google-p12-pem@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" @@ -19722,6 +19766,7 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: + encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -22556,7 +22601,12 @@ proto-list@~1.2.1: resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -protobufjs@^6.10.0, protobufjs@^6.10.2: +proto3-json-serializer@^0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.4.tgz#aa2dc4c9c9b7ea05631354b2c2e52c227539a7f0" + integrity sha512-bFzdsKU/zaTobWrRxRniMZIzzcgKYlmBWL1gAcTXZ2M7TQTGPI0JoYYs6bN7tpWj59ZCfwg7Ii/A2e8BbQGYnQ== + +protobufjs@6.11.2, protobufjs@^6.10.0, protobufjs@^6.10.2, protobufjs@^6.8.6: version "6.11.2" resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== From 16288b6580c8701ba0d3bdac26e04f597916a3ae Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 12 Oct 2021 16:16:15 +0200 Subject: [PATCH 025/245] Add class that will return a key-store based on application config Signed-off-by: Marcus Eide --- .../src/identity/KeyStores.test.ts | 86 +++++++++++++++++++ .../auth-backend/src/identity/KeyStores.ts | 75 ++++++++++++++++ plugins/auth-backend/src/identity/index.ts | 3 + 3 files changed, 164 insertions(+) create mode 100644 plugins/auth-backend/src/identity/KeyStores.test.ts create mode 100644 plugins/auth-backend/src/identity/KeyStores.ts diff --git a/plugins/auth-backend/src/identity/KeyStores.test.ts b/plugins/auth-backend/src/identity/KeyStores.test.ts new file mode 100644 index 0000000000..957c4c7953 --- /dev/null +++ b/plugins/auth-backend/src/identity/KeyStores.test.ts @@ -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 { DatabaseManager } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; + +import { MemoryKeyStore } from './MemoryKeyStore'; +import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { KeyStores } from './KeyStores'; + +describe('KeyStores', () => { + const defaultConfigOptions = { + auth: { + keyStore: { + provider: 'memory', + }, + }, + }; + const defaultConfig = new ConfigReader(defaultConfigOptions); + + it('reads auth section from config', async () => { + const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig'); + const keyStore = await KeyStores.fromConfig(defaultConfig); + + expect(keyStore).toBeInstanceOf(MemoryKeyStore); + expect(configSpy).toHaveBeenCalledWith('auth.keyStore'); + expect( + defaultConfig + .getOptionalConfig('auth.keyStore') + ?.getOptionalString('provider'), + ).toBe(defaultConfigOptions.auth.keyStore.provider); + }); + + it('can handle without auth config', async () => { + const config = new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }); + const database = + DatabaseManager.fromConfig(config).forPlugin('auth-backend'); + const keyStore = await KeyStores.fromConfig(config, { database }); + + expect(keyStore).toBeInstanceOf(DatabaseKeyStore); + }); + + it('can handle additional provider config', async () => { + const configOptions = { + auth: { + keyStore: { + provider: 'firestore', + firestore: { + projectId: 'my-project', + }, + }, + }, + }; + const config = new ConfigReader(configOptions); + const keyStore = await KeyStores.fromConfig(config); + + expect(keyStore).toBeInstanceOf(FirestoreKeyStore); + expect( + config + .getOptionalConfig('auth.keyStore') + ?.getOptionalConfig('firestore') + ?.getOptionalString('projectId'), + ).toBe(configOptions.auth.keyStore.firestore.projectId); + }); +}); diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts new file mode 100644 index 0000000000..0991f7ea7b --- /dev/null +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -0,0 +1,75 @@ +/* + * 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 { Logger } from 'winston'; + +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; + +import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { MemoryKeyStore } from './MemoryKeyStore'; +import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { KeyStore } from './types'; + +type Options = { + logger?: Logger; + database?: PluginDatabaseManager; +}; + +export class KeyStores { + /** + * Looks at the `auth.keyStore` section in the application configuration + * and returns a KeyStore store. Defaults to `postgres` + * + * @returns a KeyStore store + */ + static async fromConfig( + config: Config, + options?: Options, + ): Promise { + const { logger, database } = options ?? {}; + + const ks = config.getOptionalConfig('auth.keyStore'); + const provider = ks?.getOptionalString('provider') ?? 'postgres'; + const providerConfig = ks?.getOptionalConfig(provider); + + logger?.info(`Configuring "${provider}" as KeyStore provider`); + + if (provider === 'postgres') { + if (!database) { + throw new Error('This KeyStore provider requires a database'); + } + + return await DatabaseKeyStore.create({ + database: await database.getClient(), + }); + } + + if (provider === 'memory') { + return new MemoryKeyStore(); + } + + if (provider === 'firestore') { + return FirestoreKeyStore.create({ + projectId: providerConfig?.getOptionalString('projectId'), + keyFilename: providerConfig?.getOptionalString('keyFilename'), + path: providerConfig?.getOptionalString('path'), + }); + } + + throw new Error(`Unknown KeyStore provider: ${provider}`); + } +} diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index d88339e8e4..73858d7e07 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -18,4 +18,7 @@ export { createOidcRouter } from './router'; export { IdentityClient } from './IdentityClient'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; +export { MemoryKeyStore } from './MemoryKeyStore'; +export { FirestoreKeyStore } from './FirestoreKeyStore'; +export { KeyStores } from './KeyStores'; export type { KeyStore, TokenIssuer, TokenParams } from './types'; From 12d4abe7b25319a085e2cfeab73f52e60004f747 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 12 Oct 2021 16:18:30 +0200 Subject: [PATCH 026/245] Add project dependency to package.json Signed-off-by: Marcus Eide --- plugins/auth-backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 342718db55..8f7f623652 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -36,6 +36,7 @@ "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", "@backstage/test-utils": "^0.1.19", + "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", From 4f4e22d176d12195eb0ee007a0f25423e01d0387 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 12 Oct 2021 16:19:45 +0200 Subject: [PATCH 027/245] Use KeyStores.fromConfig() in router Signed-off-by: Marcus Eide --- plugins/auth-backend/src/service/router.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 423975aa75..8cce643987 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -29,7 +29,7 @@ import { import { NotFoundError } from '@backstage/errors'; import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; +import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import passport from 'passport'; import { Minimatch } from 'minimatch'; @@ -38,7 +38,7 @@ type ProviderFactories = { [s: string]: AuthProviderFactory }; export interface RouterOptions { logger: Logger; - database: PluginDatabaseManager; + database?: PluginDatabaseManager; config: Config; discovery: PluginEndpointDiscovery; providerFactories?: ProviderFactories; @@ -56,11 +56,9 @@ export async function createRouter({ const appUrl = config.getString('app.baseUrl'); const authUrl = await discovery.getExternalBaseUrl('auth'); + const keyStore = await KeyStores.fromConfig(config, { logger, database }); const keyDurationSeconds = 3600; - const keyStore = await DatabaseKeyStore.create({ - database: await database.getClient(), - }); const tokenIssuer = new TokenFactory({ issuer: authUrl, keyStore, From d2f755fa732b6d777f51a35630cb1110b5b9ba54 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 13 Oct 2021 09:21:30 +0200 Subject: [PATCH 028/245] Update api-reports Signed-off-by: Marcus Eide --- plugins/auth-backend/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 0bdd73f3e8..a20538e223 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -501,7 +501,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - database: PluginDatabaseManager; + database?: PluginDatabaseManager; // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) From bf259c989020210b4879aa8bec6e6024d3292814 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 13 Oct 2021 09:58:47 +0200 Subject: [PATCH 029/245] Add Firestore to vale vocab Signed-off-by: Marcus Eide --- .github/styles/vocab.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index a38baab3dc..a93f75192b 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -310,3 +310,4 @@ Zalando Zhou zoomable zsh +Firestore From ab9b4a6ea6a441e96e429def3bf43548ac089a98 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 13 Oct 2021 09:59:00 +0200 Subject: [PATCH 030/245] Add changeset Signed-off-by: Marcus Eide --- .changeset/pretty-ways-search.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pretty-ways-search.md diff --git a/.changeset/pretty-ways-search.md b/.changeset/pretty-ways-search.md new file mode 100644 index 0000000000..212c23b09a --- /dev/null +++ b/.changeset/pretty-ways-search.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add Firestore as key-store provider. +Add `auth.keyStore` section to application config. From 5ad1f523d569e020548fe17713165775d53995b8 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 13 Oct 2021 14:25:06 +0200 Subject: [PATCH 031/245] Make the create function async Signed-off-by: Marcus Eide --- .../src/identity/FirestoreKeyStore.test.ts | 24 +++++++++---------- .../src/identity/FirestoreKeyStore.ts | 4 +++- .../auth-backend/src/identity/KeyStores.ts | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts index 03fcc15a77..a12e2ed40e 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts @@ -53,14 +53,14 @@ describe('FirestoreKeyStore', () => { process.env = OLD_ENV; }); - it('can create an instance without settings', () => { - const keyStore = FirestoreKeyStore.create(); + it('can create an instance without settings', async () => { + const keyStore = await FirestoreKeyStore.create(); expect(keyStore).toBeInstanceOf(FirestoreKeyStore); }); it('can set the project id', async () => { - FirestoreKeyStore.create({ projectId: 'my-project' }); + await FirestoreKeyStore.create({ projectId: 'my-project' }); expect(Firestore).toHaveBeenCalledWith( expect.objectContaining({ @@ -70,7 +70,7 @@ describe('FirestoreKeyStore', () => { }); it('can handle keyfile file', async () => { - FirestoreKeyStore.create({ keyFilename: 'keyFile.json' }); + await FirestoreKeyStore.create({ keyFilename: 'keyFile.json' }); expect(Firestore).toHaveBeenCalledWith( expect.objectContaining({ @@ -79,9 +79,9 @@ describe('FirestoreKeyStore', () => { ); }); - it('can use default google credentials', () => { + it('can use default google credentials', async () => { process.env.GOOGLE_APPLICATION_CREDENTIALS = 'cred.json'; - FirestoreKeyStore.create(); + await FirestoreKeyStore.create(); expect(Firestore).toHaveBeenCalledWith( expect.objectContaining({ @@ -91,14 +91,14 @@ describe('FirestoreKeyStore', () => { }); it('can uses a default path', async () => { - const keyStore = FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(); await keyStore.addKey(key); expect(firestoreMock.collection).toBeCalledWith('sessions'); }); it('can set the path', async () => { - const keyStore = FirestoreKeyStore.create({ + const keyStore = await FirestoreKeyStore.create({ path: 'my-path', }); await keyStore.addKey(key); @@ -107,7 +107,7 @@ describe('FirestoreKeyStore', () => { }); it('can add keys', async () => { - const keyStore = FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(); await keyStore.addKey(key); expect(firestoreMock.collection).toBeCalledWith('sessions'); @@ -119,7 +119,7 @@ describe('FirestoreKeyStore', () => { }); it('can delete a single key', async () => { - const keyStore = FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(); await keyStore.removeKeys(['123']); expect(firestoreMock.collection).toBeCalledWith('sessions'); @@ -128,7 +128,7 @@ describe('FirestoreKeyStore', () => { }); it('can delete a multiple keys', async () => { - const keyStore = FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(); await keyStore.removeKeys(['123', '456']); expect(firestoreMock.collection).toBeCalledWith('sessions'); @@ -138,7 +138,7 @@ describe('FirestoreKeyStore', () => { }); it('can list keys', async () => { - const keyStore = FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(); const items = await keyStore.listKeys(); expect(firestoreMock.collection).toBeCalledWith('sessions'); diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts index cf8c082540..74122f2849 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -23,7 +23,9 @@ type FirestoreSettings = Settings & { }; export class FirestoreKeyStore implements KeyStore { - static create(settings?: FirestoreSettings): FirestoreKeyStore { + static async create( + settings?: FirestoreSettings, + ): Promise { const { projectId, keyFilename, path } = settings ?? {}; const database = new Firestore({ projectId, diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index 0991f7ea7b..ec236e2074 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -63,7 +63,7 @@ export class KeyStores { } if (provider === 'firestore') { - return FirestoreKeyStore.create({ + return await FirestoreKeyStore.create({ projectId: providerConfig?.getOptionalString('projectId'), keyFilename: providerConfig?.getOptionalString('keyFilename'), path: providerConfig?.getOptionalString('path'), From 33b9694f5ce8c920daae2d9370bfb29d085ad0fc Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 14 Oct 2021 14:43:56 +0200 Subject: [PATCH 032/245] Add support for more settings Signed-off-by: Marcus Eide --- plugins/auth-backend/config.d.ts | 6 ++ .../src/identity/FirestoreKeyStore.test.ts | 79 +++++++------------ .../src/identity/FirestoreKeyStore.ts | 11 +-- .../auth-backend/src/identity/KeyStores.ts | 14 ++-- 4 files changed, 44 insertions(+), 66 deletions(-) diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index b15b0da628..280d92a261 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -35,6 +35,12 @@ export interface Config { keyStore?: { provider?: 'postgres' | 'memory' | 'firestore'; firestore?: { + /** The host to connect to */ + host?: string; + /** The port to connect to */ + port?: number; + /** Whether to use SSL when connecting. */ + ssl?: boolean; /** The Google Cloud Project ID */ projectId?: string; /** diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts index a12e2ed40e..398e3c2b34 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts @@ -16,7 +16,10 @@ import { Firestore } from '@google-cloud/firestore'; -import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { + FirestoreKeyStore, + FirestoreKeyStoreSettings, +} from './FirestoreKeyStore'; import { AnyJWK } from './types'; const data = jest.fn().mockReturnValue('data'); @@ -37,7 +40,6 @@ jest.mock('@google-cloud/firestore', () => ({ })); describe('FirestoreKeyStore', () => { - const OLD_ENV = process.env; const key = { kid: '123', use: 'sig', @@ -45,72 +47,45 @@ describe('FirestoreKeyStore', () => { alg: 'Base64', } as AnyJWK; + const settings = { + projectId: 'my-project', + host: 'my-host', + port: 8080, + ssl: false, + keyFilename: 'cred.json', + }; + const path = 'my-path'; + const firestoreSettings = { ...settings, path } as FirestoreKeyStoreSettings; + beforeEach(() => { jest.clearAllMocks(); }); - afterAll(() => { - process.env = OLD_ENV; - }); - it('can create an instance without settings', async () => { const keyStore = await FirestoreKeyStore.create(); expect(keyStore).toBeInstanceOf(FirestoreKeyStore); + expect(Firestore).toHaveBeenCalledWith({}); }); - it('can set the project id', async () => { - await FirestoreKeyStore.create({ projectId: 'my-project' }); + it('can create an instance with settings', async () => { + await FirestoreKeyStore.create(firestoreSettings); - expect(Firestore).toHaveBeenCalledWith( - expect.objectContaining({ - projectId: 'my-project', - }), - ); + expect(Firestore).toHaveBeenCalledWith(settings); }); - it('can handle keyfile file', async () => { - await FirestoreKeyStore.create({ keyFilename: 'keyFile.json' }); - - expect(Firestore).toHaveBeenCalledWith( - expect.objectContaining({ - keyFilename: 'keyFile.json', - }), - ); - }); - - it('can use default google credentials', async () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = 'cred.json'; - await FirestoreKeyStore.create(); - - expect(Firestore).toHaveBeenCalledWith( - expect.objectContaining({ - keyFilename: 'cred.json', - }), - ); - }); - - it('can uses a default path', async () => { + it('can use a default path', async () => { const keyStore = await FirestoreKeyStore.create(); await keyStore.addKey(key); expect(firestoreMock.collection).toBeCalledWith('sessions'); }); - it('can set the path', async () => { - const keyStore = await FirestoreKeyStore.create({ - path: 'my-path', - }); - await keyStore.addKey(key); - - expect(firestoreMock.collection).toBeCalledWith('my-path'); - }); - it('can add keys', async () => { - const keyStore = await FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(firestoreSettings); await keyStore.addKey(key); - expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.collection).toBeCalledWith(path); expect(firestoreMock.doc).toBeCalledWith(key.kid); expect(firestoreMock.set).toHaveBeenCalledWith({ kid: key.kid, @@ -119,29 +94,29 @@ describe('FirestoreKeyStore', () => { }); it('can delete a single key', async () => { - const keyStore = await FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(firestoreSettings); await keyStore.removeKeys(['123']); - expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.collection).toBeCalledWith(path); expect(firestoreMock.doc).toBeCalledWith('123'); expect(firestoreMock.delete).toBeCalledTimes(1); }); it('can delete a multiple keys', async () => { - const keyStore = await FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(firestoreSettings); await keyStore.removeKeys(['123', '456']); - expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.collection).toBeCalledWith(path); expect(firestoreMock.doc).toBeCalledWith('123'); expect(firestoreMock.doc).toBeCalledWith('456'); expect(firestoreMock.delete).toBeCalledTimes(2); }); it('can list keys', async () => { - const keyStore = await FirestoreKeyStore.create(); + const keyStore = await FirestoreKeyStore.create(firestoreSettings); const items = await keyStore.listKeys(); - expect(firestoreMock.collection).toBeCalledWith('sessions'); + expect(firestoreMock.collection).toBeCalledWith(path); expect(firestoreMock.get).toBeCalledTimes(1); expect(data).toBeCalledTimes(1); expect(toDate).toBeCalledTimes(1); diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts index 74122f2849..f606bacd38 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -18,19 +18,16 @@ import { Firestore, Settings } from '@google-cloud/firestore'; import { AnyJWK, KeyStore, StoredKey } from './types'; -type FirestoreSettings = Settings & { +export type FirestoreKeyStoreSettings = Settings & { path?: string; }; export class FirestoreKeyStore implements KeyStore { static async create( - settings?: FirestoreSettings, + settings?: FirestoreKeyStoreSettings, ): Promise { - const { projectId, keyFilename, path } = settings ?? {}; - const database = new Firestore({ - projectId, - keyFilename: keyFilename ?? process.env.GOOGLE_APPLICATION_CREDENTIALS, - }); + const { path, ...firestoreSettings } = settings ?? {}; + const database = new Firestore(firestoreSettings); return new FirestoreKeyStore(database, path ?? 'sessions'); } diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index ec236e2074..1b19bde911 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -21,7 +21,10 @@ import { Config } from '@backstage/config'; import { DatabaseKeyStore } from './DatabaseKeyStore'; import { MemoryKeyStore } from './MemoryKeyStore'; -import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { + FirestoreKeyStore, + FirestoreKeyStoreSettings, +} from './FirestoreKeyStore'; import { KeyStore } from './types'; type Options = { @@ -44,7 +47,6 @@ export class KeyStores { const ks = config.getOptionalConfig('auth.keyStore'); const provider = ks?.getOptionalString('provider') ?? 'postgres'; - const providerConfig = ks?.getOptionalConfig(provider); logger?.info(`Configuring "${provider}" as KeyStore provider`); @@ -63,11 +65,9 @@ export class KeyStores { } if (provider === 'firestore') { - return await FirestoreKeyStore.create({ - projectId: providerConfig?.getOptionalString('projectId'), - keyFilename: providerConfig?.getOptionalString('keyFilename'), - path: providerConfig?.getOptionalString('path'), - }); + const settings = ks?.getOptional(provider) as FirestoreKeyStoreSettings; + + return await FirestoreKeyStore.create(settings); } throw new Error(`Unknown KeyStore provider: ${provider}`); From 01a4c5f9987937746b45dc2c98c35890d343e8ca Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 15 Oct 2021 10:36:49 +0100 Subject: [PATCH 033/245] fixes tests Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/src/service/router.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5f855a6e88..afc9cee986 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -25,13 +25,8 @@ import { StorageTaskBroker, TaskWorker, } from '../scaffolder/tasks'; -import { - TemplateActionRegistry, - TemplateAction, - createBuiltinActions, - TaskBroker, - TaskSpec, -} from '../scaffolder'; +import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry'; +import { TaskBroker } from '../scaffolder'; import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; import { ContainerRunner, @@ -44,9 +39,12 @@ import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrations } from '@backstage/integration'; +import { TemplateAction } from '../scaffolder/actions'; +import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; import { LegacyWorkflowRunner } from '../scaffolder/tasks/LegacyWorkflowRunner'; import { DefaultWorkflowRunner } from '../scaffolder/tasks/DefaultWorkflowRunner'; +import { TaskSpec } from '../scaffolder/tasks/types'; export interface RouterOptions { logger: Logger; From b149e9429030ac8aa53d939390faccc8272e7d03 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Fri, 15 Oct 2021 16:08:06 -0400 Subject: [PATCH 034/245] fix(catalog:register): support optional locations Signed-off-by: Phil Kuang --- .changeset/fair-jeans-heal.md | 5 ++ .../actions/builtin/catalog/register.test.ts | 38 ++++++++++++++ .../actions/builtin/catalog/register.ts | 52 +++++++++++++------ 3 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 .changeset/fair-jeans-heal.md diff --git a/.changeset/fair-jeans-heal.md b/.changeset/fair-jeans-heal.md new file mode 100644 index 0000000000..48c0adbfc2 --- /dev/null +++ b/.changeset/fair-jeans-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Allow `catalog:register` action to register optional locations diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts index eb704c729e..e1f8823487 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts @@ -174,4 +174,42 @@ describe('catalog:register', () => { 'http://foo/var', ); }); + + it('should ignore failures when dry running the location in the catalog if `optional` is set', async () => { + addLocation + .mockResolvedValueOnce({ + entities: [], + }) + .mockRejectedValueOnce(new Error('Not found')); + await action.handler({ + ...mockContext, + input: { + catalogInfoUrl: 'http://foo/var', + optional: true, + }, + }); + + expect(addLocation).toHaveBeenNthCalledWith( + 1, + { + type: 'url', + target: 'http://foo/var', + }, + {}, + ); + expect(addLocation).toHaveBeenNthCalledWith( + 2, + { + dryRun: true, + type: 'url', + target: 'http://foo/var', + }, + {}, + ); + + expect(mockContext.output).toBeCalledWith( + 'catalogInfoUrl', + 'http://foo/var', + ); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 9d4961bfd9..0c08131925 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -27,8 +27,8 @@ export function createCatalogRegisterAction(options: { const { catalogClient, integrations } = options; return createTemplateAction< - | { catalogInfoUrl: string } - | { repoContentsUrl: string; catalogInfoPath?: string } + | { catalogInfoUrl: string; optional?: boolean } + | { repoContentsUrl: string; catalogInfoPath?: string; optional?: boolean } >({ id: 'catalog:register', description: @@ -46,6 +46,12 @@ export function createCatalogRegisterAction(options: { 'An absolute URL pointing to the catalog info file location', type: 'string', }, + optional: { + title: 'Optional', + description: + 'Permit the registered location to optionally exist. Default: false', + type: 'boolean', + }, }, }, { @@ -64,6 +70,12 @@ export function createCatalogRegisterAction(options: { 'A relative path from the repo root pointing to the catalog info file, defaults to /catalog-info.yaml', type: 'string', }, + optional: { + title: 'Optional', + description: + 'Permit the registered location to optionally exist. Default: false', + type: 'boolean', + }, }, }, ], @@ -100,22 +112,30 @@ export function createCatalogRegisterAction(options: { }, ctx.token ? { token: ctx.token } : {}, ); - const result = await catalogClient.addLocation( - { - dryRun: true, - type: 'url', - target: catalogInfoUrl, - }, - ctx.token ? { token: ctx.token } : {}, - ); - if (result.entities.length > 0) { - const { entities } = result; - const entity = - entities.find(e => !e.metadata.name.startsWith('generated-')) ?? - entities[0]; - ctx.output('entityRef', stringifyEntityRef(entity)); + try { + const result = await catalogClient.addLocation( + { + dryRun: true, + type: 'url', + target: catalogInfoUrl, + }, + ctx.token ? { token: ctx.token } : {}, + ); + + if (result.entities.length > 0) { + const { entities } = result; + const entity = + entities.find(e => !e.metadata.name.startsWith('generated-')) ?? + entities[0]; + ctx.output('entityRef', stringifyEntityRef(entity)); + } + } catch (e) { + if (!input.optional) { + throw e; + } } + ctx.output('catalogInfoUrl', catalogInfoUrl); }, }); From 3842901ddadc72de7748471bc25c37129913236f Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Sun, 17 Oct 2021 21:22:42 -0600 Subject: [PATCH 035/245] api-reports Signed-off-by: Tim Hansen --- plugins/catalog-react/api-report.md | 13 ++++++++++++- .../EntityTypePicker/EntityTypePicker.tsx | 6 +++--- .../src/components/EntityTypePicker/index.ts | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 7f50e87e03..705ce3feab 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -645,10 +645,21 @@ export class EntityTypeFilter implements EntityFilter { readonly value: string | string[]; } +// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EntityTypeFilterProps = { + initialFilter?: string; + hidden?: boolean; +}; + // Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityTypePicker: () => JSX.Element | null; +export const EntityTypePicker: ({ + hidden, + initialFilter, +}: EntityTypeFilterProps) => JSX.Element | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index d8fa6b6b90..4f5cc96463 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,7 +22,7 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -type EntityTypeFilterProps = { +export type EntityTypeFilterProps = { initialFilter?: string; hidden?: boolean; }; @@ -57,7 +57,7 @@ export const EntityTypePicker = ({ })), ]; - return !hidden ? ( + return hidden ? null : ( , From db58c2e879745a9401f1c7a4ab6d1a1e03ffdb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 Oct 2021 09:52:17 +0200 Subject: [PATCH 044/245] mark the observable types public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/core-types/api-report.md | 6 ------ packages/core-types/src/observable.ts | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core-types/api-report.md b/packages/core-types/api-report.md index bd4d145a10..09611f63e1 100644 --- a/packages/core-types/api-report.md +++ b/packages/core-types/api-report.md @@ -17,8 +17,6 @@ export type JsonPrimitive = number | string | boolean | null; // @public export type JsonValue = JsonObject | JsonArray | JsonPrimitive; -// Warning: (ae-missing-release-tag) "Observable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type Observable = { [Symbol.observable](): Observable; @@ -30,8 +28,6 @@ export type Observable = { ): Subscription; }; -// Warning: (ae-missing-release-tag) "Observer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type Observer = { next?(value: T): void; @@ -39,8 +35,6 @@ export type Observer = { complete?(): void; }; -// Warning: (ae-missing-release-tag) "Subscription" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type Subscription = { unsubscribe(): void; diff --git a/packages/core-types/src/observable.ts b/packages/core-types/src/observable.ts index 367fa087e0..f7292c293c 100644 --- a/packages/core-types/src/observable.ts +++ b/packages/core-types/src/observable.ts @@ -16,6 +16,8 @@ /** * Observer interface for consuming an Observer, see TC39. + * + * @public */ export type Observer = { next?(value: T): void; @@ -25,6 +27,8 @@ export type Observer = { /** * Subscription returned when subscribing to an Observable, see TC39. + * + * @public */ export type Subscription = { /** @@ -53,6 +57,8 @@ declare global { * * This is used as a common return type for observable values and can be created * using many different observable implementations, such as zen-observable or RxJS 5. + * + * @public */ export type Observable = { [Symbol.observable](): Observable; From 7e707db187c94d66570c0a36215726a145412e93 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 21 Oct 2021 10:01:12 +0200 Subject: [PATCH 045/245] Undo making database optional Signed-off-by: Marcus Eide --- plugins/auth-backend/api-report.md | 2 +- plugins/auth-backend/src/service/router.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index a20538e223..0bdd73f3e8 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -501,7 +501,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - database?: PluginDatabaseManager; + database: PluginDatabaseManager; // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 8cce643987..23311f5e56 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -38,7 +38,7 @@ type ProviderFactories = { [s: string]: AuthProviderFactory }; export interface RouterOptions { logger: Logger; - database?: PluginDatabaseManager; + database: PluginDatabaseManager; config: Config; discovery: PluginEndpointDiscovery; providerFactories?: ProviderFactories; From 6237e636eb3f4cab189b6503298f0f45a878b31b Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 21 Oct 2021 10:30:29 +0200 Subject: [PATCH 046/245] Be more specific when accessing firestore provider config Signed-off-by: Marcus Eide --- .../src/identity/KeyStores.test.ts | 10 ++++++++++ .../auth-backend/src/identity/KeyStores.ts | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/identity/KeyStores.test.ts b/plugins/auth-backend/src/identity/KeyStores.test.ts index bb91d2cae9..f30263e27f 100644 --- a/plugins/auth-backend/src/identity/KeyStores.test.ts +++ b/plugins/auth-backend/src/identity/KeyStores.test.ts @@ -63,6 +63,7 @@ describe('KeyStores', () => { it('can handle additional provider config', async () => { jest.spyOn(FirestoreKeyStore, 'verifyConnection').mockResolvedValue(); + const createSpy = jest.spyOn(FirestoreKeyStore, 'create'); const configOptions = { auth: { @@ -70,6 +71,12 @@ describe('KeyStores', () => { provider: 'firestore', firestore: { projectId: 'my-project', + keyFilename: 'cred.json', + path: 'my-path', + timeout: 100, + host: 'localhost', + port: 8088, + ssl: false, }, }, }, @@ -78,6 +85,9 @@ describe('KeyStores', () => { const keyStore = await KeyStores.fromConfig(config); expect(keyStore).toBeInstanceOf(FirestoreKeyStore); + expect(createSpy).toHaveBeenCalledWith( + configOptions.auth.keyStore.firestore, + ); expect( config .getOptionalConfig('auth.keyStore') diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index 8ac1cf1ec6..5ec63f5439 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -21,10 +21,7 @@ import { Config } from '@backstage/config'; import { DatabaseKeyStore } from './DatabaseKeyStore'; import { MemoryKeyStore } from './MemoryKeyStore'; -import { - FirestoreKeyStore, - FirestoreKeyStoreSettings, -} from './FirestoreKeyStore'; +import { FirestoreKeyStore } from './FirestoreKeyStore'; import { KeyStore } from './types'; type Options = { @@ -65,8 +62,18 @@ export class KeyStores { } if (provider === 'firestore') { - const settings = ks?.getOptional(provider) as FirestoreKeyStoreSettings; - const keyStore = await FirestoreKeyStore.create(settings); + const settings = ks?.getConfig(provider); + + const keyStore = await FirestoreKeyStore.create({ + projectId: settings?.getOptionalString('projectId'), + keyFilename: settings?.getOptionalString('keyFilename'), + host: settings?.getOptionalString('host'), + port: settings?.getOptionalNumber('port'), + ssl: settings?.getOptionalBoolean('ssl'), + path: settings?.getOptionalString('path'), + timeout: settings?.getOptionalNumber('timeout'), + }); + await FirestoreKeyStore.verifyConnection(keyStore, logger); return keyStore; From 34da1574a3d768e7076ee2160e882e9b1d010d6b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 21 Oct 2021 13:48:49 +0200 Subject: [PATCH 047/245] auth-backend: rename postgres keystore provider to database Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/config.d.ts | 2 +- plugins/auth-backend/src/identity/KeyStores.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 6f31cd07df..25da7fa332 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -33,7 +33,7 @@ export interface Config { /** To control how to store JWK data in auth-backend */ keyStore?: { - provider?: 'postgres' | 'memory' | 'firestore'; + provider?: 'database' | 'memory' | 'firestore'; firestore?: { /** The host to connect to */ host?: string; diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index 5ec63f5439..74e60a0502 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -32,7 +32,7 @@ type Options = { export class KeyStores { /** * Looks at the `auth.keyStore` section in the application configuration - * and returns a KeyStore store. Defaults to `postgres` + * and returns a KeyStore store. Defaults to `database` * * @returns a KeyStore store */ @@ -43,11 +43,11 @@ export class KeyStores { const { logger, database } = options ?? {}; const ks = config.getOptionalConfig('auth.keyStore'); - const provider = ks?.getOptionalString('provider') ?? 'postgres'; + const provider = ks?.getOptionalString('provider') ?? 'database'; logger?.info(`Configuring "${provider}" as KeyStore provider`); - if (provider === 'postgres') { + if (provider === 'database') { if (!database) { throw new Error('This KeyStore provider requires a database'); } From 0de67c4d2ae21fb7ed891b93ea43dcef28946e32 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Thu, 21 Oct 2021 13:54:44 +0200 Subject: [PATCH 048/245] Handle single-item array filter in Lunr search engine Signed-off-by: Tomasz Szuba --- .changeset/spicy-foxes-joke.md | 5 +++++ .../search-backend-node/src/engines/LunrSearchEngine.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/spicy-foxes-joke.md diff --git a/.changeset/spicy-foxes-joke.md b/.changeset/spicy-foxes-joke.md new file mode 100644 index 0000000000..c1ed318f6d --- /dev/null +++ b/.changeset/spicy-foxes-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Handle special case when filter array has single value optimizing Lunr search behaviour. diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index f22762cb9f..ea51ffe8e1 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -80,11 +80,17 @@ export class LunrSearchEngine implements SearchEngine { }); if (filters) { - Object.entries(filters).forEach(([field, value]) => { + Object.entries(filters).forEach(([field, fieldValue]) => { if (!q.allFields.includes(field)) { // Throw for unknown field, as this will be a non match throw new Error(`unrecognised field ${field}`); } + // Arrays are poorly supported, but we can make it better for single-item arrays, + // which should be a common case + const value = + Array.isArray(fieldValue) && fieldValue.length === 1 + ? fieldValue[0] + : fieldValue; // Require that the given field has the given value if (['string', 'number', 'boolean'].includes(typeof value)) { From 428061d7e56451750d02a9f8fcb2f6101338f347 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Thu, 21 Oct 2021 14:04:58 +0200 Subject: [PATCH 049/245] Add tests Signed-off-by: Tomasz Szuba --- .../src/engines/LunrSearchEngine.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index a2cbb4ecc3..cbe48ef5a7 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -192,6 +192,37 @@ describe('LunrSearchEngine', () => { }); }); + it('should handle single-item array filter as scalar value', async () => { + const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + logger: getVoidLogger(), + }); + const translatorUnderTest = inspectableSearchEngine.getTranslator(); + + const actualTranslatedQuery = translatorUnderTest({ + term: 'testTerm', + filters: { kind: ['testKind'] }, + }) as ConcreteLunrQuery; + + expect(actualTranslatedQuery).toMatchObject({ + documentTypes: undefined, + lunrQueryBuilder: expect.any(Function), + }); + + const query: jest.Mocked = { + allFields: ['kind'], + clauses: [], + term: jest.fn(), + clause: jest.fn(), + }; + + actualTranslatedQuery.lunrQueryBuilder.bind(query)(query); + + expect(query.term).toBeCalledWith(lunr.tokenizer('testKind'), { + fields: ['kind'], + presence: lunr.Query.presence.REQUIRED, + }); + }); + it('should return translated query with multiple filters', async () => { const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ logger: getVoidLogger(), From bf5dfa2708f685b56e11c14c449d5a2ba5413e24 Mon Sep 17 00:00:00 2001 From: Ivan Stoiev Date: Thu, 21 Oct 2021 09:38:28 -0300 Subject: [PATCH 050/245] update datatogRum browser agent version Signed-off-by: Ivan Stoiev Signed-off-by: Ivan Stoiev --- packages/app/public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/public/index.html b/packages/app/public/index.html index da4d3cf3b8..d86f09f0b0 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -83,7 +83,7 @@ window, document, 'script', - 'https://www.datadoghq-browser-agent.com/datadog-rum.js', + 'https://www.datadoghq-browser-agent.com/datadog-rum-v3.js', 'DD_RUM', ); DD_RUM.onReady(function () { From a369f19e7eed5b40bff1b225cbe475c5ad170b0b Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Thu, 21 Oct 2021 15:13:30 +0200 Subject: [PATCH 051/245] Rename changeset Signed-off-by: Tomasz Szuba --- .changeset/{spicy-foxes-joke.md => search-spicy-foxes-joke.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changeset/{spicy-foxes-joke.md => search-spicy-foxes-joke.md} (100%) diff --git a/.changeset/spicy-foxes-joke.md b/.changeset/search-spicy-foxes-joke.md similarity index 100% rename from .changeset/spicy-foxes-joke.md rename to .changeset/search-spicy-foxes-joke.md From 447c514899c84dc4a53439e8ae121b37831f6607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 Oct 2021 15:05:07 +0200 Subject: [PATCH 052/245] rename to just types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/rotten-melons-carry.md | 2 +- packages/config/api-report.md | 8 ++++---- packages/config/package.json | 2 +- packages/config/src/deprecatedTypes.ts | 12 ++++++------ packages/config/src/reader.ts | 2 +- packages/config/src/types.ts | 2 +- packages/core-app-api/api-report.md | 2 +- packages/core-plugin-api/api-report.md | 6 +++--- packages/core-plugin-api/package.json | 2 +- .../core-plugin-api/src/apis/definitions/AlertApi.ts | 2 +- .../src/apis/definitions/AppThemeApi.ts | 2 +- .../core-plugin-api/src/apis/definitions/ErrorApi.ts | 2 +- .../src/apis/definitions/OAuthRequestApi.ts | 2 +- .../src/apis/definitions/StorageApi.ts | 2 +- .../core-plugin-api/src/apis/definitions/auth.ts | 2 +- packages/core-plugin-api/src/deprecatedTypes.ts | 8 ++++---- packages/{core-types => types}/.eslintrc.js | 0 packages/{core-types => types}/README.md | 2 +- packages/{core-types => types}/api-report.md | 2 +- packages/{core-types => types}/package.json | 6 +++--- packages/{core-types => types}/src/index.ts | 2 +- packages/{core-types => types}/src/json.ts | 0 packages/{core-types => types}/src/observable.ts | 0 packages/{core-types => types}/src/setupTests.ts | 0 plugins/scaffolder-backend/api-report.md | 4 ++-- 25 files changed, 37 insertions(+), 37 deletions(-) rename packages/{core-types => types}/.eslintrc.js (100%) rename packages/{core-types => types}/README.md (82%) rename packages/{core-types => types}/api-report.md (94%) rename packages/{core-types => types}/package.json (85%) rename packages/{core-types => types}/src/index.ts (94%) rename packages/{core-types => types}/src/json.ts (100%) rename packages/{core-types => types}/src/observable.ts (100%) rename packages/{core-types => types}/src/setupTests.ts (100%) diff --git a/.changeset/rotten-melons-carry.md b/.changeset/rotten-melons-carry.md index 5e9f849d2f..2f1429c399 100644 --- a/.changeset/rotten-melons-carry.md +++ b/.changeset/rotten-melons-carry.md @@ -5,4 +5,4 @@ '@backstage/plugin-scaffolder-backend': patch --- -Start using the new `@backstage/core-types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. diff --git a/packages/config/api-report.md b/packages/config/api-report.md index 116231ac72..205db6ef2b 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -3,10 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import type { JsonArray as JsonArray_2 } from '@backstage/core-types'; -import { JsonObject as JsonObject_2 } from '@backstage/core-types'; -import type { JsonPrimitive as JsonPrimitive_2 } from '@backstage/core-types'; -import { JsonValue as JsonValue_2 } from '@backstage/core-types'; +import type { JsonArray as JsonArray_2 } from '@backstage/types'; +import { JsonObject as JsonObject_2 } from '@backstage/types'; +import type { JsonPrimitive as JsonPrimitive_2 } from '@backstage/types'; +import { JsonValue as JsonValue_2 } from '@backstage/types'; // @public export type AppConfig = { diff --git a/packages/config/package.json b/packages/config/package.json index 7cd39eb2b0..da741b7a3b 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-types": "^0.1.0", + "@backstage/types": "^0.1.0", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/config/src/deprecatedTypes.ts b/packages/config/src/deprecatedTypes.ts index 17876f60eb..823825451b 100644 --- a/packages/config/src/deprecatedTypes.ts +++ b/packages/config/src/deprecatedTypes.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -// Temporarily re-export the JSON types from @backstage/core-types +// Temporarily re-export the JSON types from @backstage/types import type { JsonArray as CoreJsonArray, JsonObject as CoreJsonObject, JsonPrimitive as CoreJsonPrimitive, JsonValue as CoreJsonValue, -} from '@backstage/core-types'; +} from '@backstage/types'; /** * A type representing all allowed JSON primitive values. * * @public - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type JsonPrimitive = CoreJsonPrimitive; @@ -34,7 +34,7 @@ export type JsonPrimitive = CoreJsonPrimitive; * A type representing all allowed JSON object values. * * @public - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type JsonObject = CoreJsonObject; @@ -42,7 +42,7 @@ export type JsonObject = CoreJsonObject; * A type representing all allowed JSON array values. * * @public - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type JsonArray = CoreJsonArray; @@ -50,6 +50,6 @@ export type JsonArray = CoreJsonArray; * A type representing all allowed JSON values. * * @public - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type JsonValue = CoreJsonValue; diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index fee1b34569..bc8a860c85 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue, JsonObject } from '@backstage/core-types'; +import { JsonValue, JsonObject } from '@backstage/types'; import { AppConfig, Config } from './types'; import cloneDeep from 'lodash/cloneDeep'; import mergeWith from 'lodash/mergeWith'; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index e98dee9fa7..a543233277 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/core-types'; +import { JsonObject, JsonValue } from '@backstage/types'; /** * A serialized form of configuration data that carries additional context. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index bfeaa7f678..f47ad2e791 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -40,7 +40,7 @@ import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/core-plugin-api'; -import { Observable as Observable_2 } from '@backstage/core-types'; +import { Observable as Observable_2 } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 765cdaa21c..0347b4b018 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -8,12 +8,12 @@ import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; -import { Observable as Observable_2 } from '@backstage/core-types'; -import { Observer as Observer_2 } from '@backstage/core-types'; +import { Observable as Observable_2 } from '@backstage/types'; +import { Observer as Observer_2 } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; -import { Subscription as Subscription_2 } from '@backstage/core-types'; +import { Subscription as Subscription_2 } from '@backstage/types'; import { SvgIconProps } from '@material-ui/core'; // Warning: (ae-missing-release-tag) "AlertApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index fbb2760e59..938ddc63e3 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -30,8 +30,8 @@ }, "dependencies": { "@backstage/config": "^0.1.9", - "@backstage/core-types": "^0.1.0", "@backstage/theme": "^0.2.9", + "@backstage/types": "^0.1.0", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@types/react": "*", diff --git a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts index ecd12c533a..7110c1d9f8 100644 --- a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts @@ -15,7 +15,7 @@ */ import { createApiRef, ApiRef } from '../system'; -import { Observable } from '@backstage/core-types'; +import { Observable } from '@backstage/types'; export type AlertMessage = { message: string; diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index 8608b53593..5ac16f83fd 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -16,7 +16,7 @@ import { ApiRef, createApiRef } from '../system'; import { BackstageTheme } from '@backstage/theme'; -import { Observable } from '@backstage/core-types'; +import { Observable } from '@backstage/types'; /** * Describes a theme provided by the app. diff --git a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts index 6fd47ac966..69c331bc06 100644 --- a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts @@ -15,7 +15,7 @@ */ import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/core-types'; +import { Observable } from '@backstage/types'; /** * Mirrors the JavaScript Error class, for the purpose of diff --git a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 43351edaf8..14609f5b3a 100644 --- a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -15,7 +15,7 @@ */ import { IconComponent } from '../../icons/types'; -import { Observable } from '@backstage/core-types'; +import { Observable } from '@backstage/types'; import { ApiRef, createApiRef } from '../system'; /** diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index 7951403040..f9a52a0bf4 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -15,7 +15,7 @@ */ import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/core-types'; +import { Observable } from '@backstage/types'; export type StorageValueChange = { key: string; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 314f2955cf..e1c56279d9 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -15,7 +15,7 @@ */ import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/core-types'; +import { Observable } from '@backstage/types'; /** * This file contains declarations for common interfaces of auth-related APIs. diff --git a/packages/core-plugin-api/src/deprecatedTypes.ts b/packages/core-plugin-api/src/deprecatedTypes.ts index 598f666317..605dc72ee3 100644 --- a/packages/core-plugin-api/src/deprecatedTypes.ts +++ b/packages/core-plugin-api/src/deprecatedTypes.ts @@ -18,19 +18,19 @@ import { Observer as CoreObserver, Subscription as CoreSubscription, Observable as CoreObservable, -} from '@backstage/core-types'; +} from '@backstage/types'; /** * Observer interface for consuming an Observer, see TC39. * - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type Observer = CoreObserver; /** * Subscription returned when subscribing to an Observable, see TC39. * - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type Subscription = CoreSubscription; @@ -42,6 +42,6 @@ export type Subscription = CoreSubscription; * This is used as a common return type for observable values and can be created * using many different observable implementations, such as zen-observable or RxJS 5. * - * @deprecated Please use the same type from `@backstage/core-types` instead + * @deprecated Please use the same type from `@backstage/types` instead */ export type Observable = CoreObservable; diff --git a/packages/core-types/.eslintrc.js b/packages/types/.eslintrc.js similarity index 100% rename from packages/core-types/.eslintrc.js rename to packages/types/.eslintrc.js diff --git a/packages/core-types/README.md b/packages/types/README.md similarity index 82% rename from packages/core-types/README.md rename to packages/types/README.md index 52bd1fa7a5..4462890ed2 100644 --- a/packages/core-types/README.md +++ b/packages/types/README.md @@ -1,4 +1,4 @@ -# Common core types used within Backstage +# Common TypeScript types used within Backstage Contains some common types that are used widely within the rest of the Backstage project and in plugins. diff --git a/packages/core-types/api-report.md b/packages/types/api-report.md similarity index 94% rename from packages/core-types/api-report.md rename to packages/types/api-report.md index 09611f63e1..43e3cfee0e 100644 --- a/packages/core-types/api-report.md +++ b/packages/types/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/core-types" +## API Report File for "@backstage/types" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/packages/core-types/package.json b/packages/types/package.json similarity index 85% rename from packages/core-types/package.json rename to packages/types/package.json index ce6fc05fb4..8274c1ce74 100644 --- a/packages/core-types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { - "name": "@backstage/core-types", - "description": "Common core types used within Backstage", + "name": "@backstage/types", + "description": "Common TypeScript types used within Backstage", "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", @@ -16,7 +16,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "packages/core-types" + "directory": "packages/types" }, "keywords": [ "backstage" diff --git a/packages/core-types/src/index.ts b/packages/types/src/index.ts similarity index 94% rename from packages/core-types/src/index.ts rename to packages/types/src/index.ts index 9e7a33c5f2..1f1b13c349 100644 --- a/packages/core-types/src/index.ts +++ b/packages/types/src/index.ts @@ -15,7 +15,7 @@ */ /** - * Common core types used within Backstage + * Common TypeScript types used within Backstage * * @packageDocumentation */ diff --git a/packages/core-types/src/json.ts b/packages/types/src/json.ts similarity index 100% rename from packages/core-types/src/json.ts rename to packages/types/src/json.ts diff --git a/packages/core-types/src/observable.ts b/packages/types/src/observable.ts similarity index 100% rename from packages/core-types/src/observable.ts rename to packages/types/src/observable.ts diff --git a/packages/core-types/src/setupTests.ts b/packages/types/src/setupTests.ts similarity index 100% rename from packages/core-types/src/setupTests.ts rename to packages/types/src/setupTests.ts diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 9dd86eeddc..07c6c7a57d 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -15,9 +15,9 @@ import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { JsonObject } from '@backstage/config'; -import { JsonObject as JsonObject_2 } from '@backstage/core-types'; +import { JsonObject as JsonObject_2 } from '@backstage/types'; import { JsonValue } from '@backstage/config'; -import { JsonValue as JsonValue_2 } from '@backstage/core-types'; +import { JsonValue as JsonValue_2 } from '@backstage/types'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Octokit } from '@octokit/rest'; From b61c50a12fa7b6ba1785556906172b74ea7f3660 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Thu, 21 Oct 2021 14:21:35 +0100 Subject: [PATCH 053/245] fix k8s crd regression (#7728) Signed-off-by: mclarke47 --- .changeset/eleven-brooms-add.md | 5 +++++ plugins/kubernetes-backend/src/service/KubernetesBuilder.ts | 1 + plugins/kubernetes-backend/src/service/KubernetesFetcher.ts | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/eleven-brooms-add.md diff --git a/.changeset/eleven-brooms-add.md b/.changeset/eleven-brooms-add.md new file mode 100644 index 0000000000..4554196ba8 --- /dev/null +++ b/.changeset/eleven-brooms-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Fix Kubernetes plugin custom objects lookup regression diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index bde538bf55..11cca4917f 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -124,6 +124,7 @@ export class KubernetesBuilder { group: c.getString('group'), apiVersion: c.getString('apiVersion'), plural: c.getString('plural'), + objectType: 'customresources', } as CustomResource), ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index a08f339a20..308b7a10be 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -148,7 +148,10 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { labelSelector, ) .then(r => { - return { type: objectType, resources: (r.body as any).items }; + return { + type: objectType, + resources: (r.body as any).items, + }; }); } } From bf5090e59dc64bbea40b74b12b2ef0cfe5291432 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Oct 2021 13:28:13 +0000 Subject: [PATCH 054/245] Version Packages --- .changeset/blue-dingos-repeat.md | 5 --- .changeset/brave-spoons-grow.md | 5 --- .changeset/brave-waves-explain.md | 15 --------- .changeset/brown-paws-marry.md | 5 --- .changeset/chilled-tools-worry.md | 5 --- .changeset/eighty-forks-fry.md | 5 --- .changeset/eleven-brooms-add.md | 5 --- .changeset/fair-jeans-heal.md | 5 --- .changeset/few-fishes-heal.md | 9 ------ .changeset/four-days-sneeze.md | 6 ---- .changeset/giant-phones-exercise.md | 5 --- .changeset/hip-pens-love.md | 5 --- .changeset/hot-wolves-share.md | 5 --- .changeset/khaki-pants-vanish.md | 5 --- .changeset/large-dragons-design.md | 5 --- .changeset/lucky-plums-brake.md | 5 --- .changeset/lucky-pumas-cross.md | 5 --- .changeset/mean-glasses-play.md | 5 --- .changeset/moody-bats-matter.md | 5 --- .changeset/nice-pillows-move.md | 5 --- .changeset/polite-eyes-punch.md | 5 --- .changeset/pretty-ways-search.md | 6 ---- .changeset/quick-frogs-double.md | 6 ---- .changeset/quiet-parrots-occur.md | 5 --- .changeset/rare-cooks-play.md | 14 --------- .changeset/real-mice-beg.md | 7 ----- .changeset/rude-comics-float.md | 5 --- .changeset/selfish-papayas-watch.md | 5 --- .changeset/sharp-crabs-stare.md | 5 --- .changeset/swift-pugs-serve.md | 12 ------- .changeset/techdocs-dull-glasses-decide.md | 5 --- .changeset/tiny-panthers-jump.md | 22 ------------- .changeset/twelve-candles-mix.md | 5 --- .changeset/two-ties-eat.md | 5 --- .changeset/wise-camels-run.md | 15 --------- .changeset/wise-hats-remember.md | 5 --- packages/app/CHANGELOG.md | 19 ++++++++++++ packages/app/package.json | 28 ++++++++--------- packages/backend-common/CHANGELOG.md | 10 ++++++ packages/backend-common/package.json | 8 ++--- packages/backend-test-utils/CHANGELOG.md | 8 +++++ packages/backend-test-utils/package.json | 8 ++--- packages/backend/package.json | 24 +++++++------- packages/catalog-client/package.json | 6 ++-- packages/catalog-model/CHANGELOG.md | 8 +++++ packages/catalog-model/package.json | 6 ++-- packages/cli/CHANGELOG.md | 23 ++++++++++++++ packages/cli/package.json | 14 ++++----- packages/codemods/CHANGELOG.md | 9 ++++++ packages/codemods/package.json | 2 +- packages/config-loader/CHANGELOG.md | 14 +++++++++ packages/config-loader/package.json | 4 +-- packages/core-app-api/CHANGELOG.md | 15 +++++++++ packages/core-app-api/package.json | 8 ++--- packages/core-components/CHANGELOG.md | 11 +++++++ packages/core-components/package.json | 10 +++--- packages/core-plugin-api/CHANGELOG.md | 12 +++++++ packages/core-plugin-api/package.json | 6 ++-- packages/create-app/CHANGELOG.md | 31 +++++++++++++++++++ packages/create-app/package.json | 2 +- packages/dev-utils/package.json | 12 +++---- packages/errors/CHANGELOG.md | 9 ++++++ packages/errors/package.json | 4 +-- packages/integration-react/package.json | 6 ++-- packages/integration/package.json | 4 +-- packages/search-common/package.json | 2 +- packages/techdocs-common/CHANGELOG.md | 11 +++++++ packages/techdocs-common/package.json | 10 +++--- packages/test-utils/package.json | 6 ++-- packages/theme/package.json | 2 +- packages/version-bridge/package.json | 2 +- plugins/allure/package.json | 12 +++---- plugins/analytics-module-ga/package.json | 8 ++--- plugins/api-docs/package.json | 14 ++++----- plugins/app-backend/CHANGELOG.md | 8 +++++ plugins/app-backend/package.json | 8 ++--- plugins/auth-backend/CHANGELOG.md | 20 ++++++++++++ plugins/auth-backend/package.json | 10 +++--- plugins/azure-devops-backend/CHANGELOG.md | 8 +++++ plugins/azure-devops-backend/package.json | 6 ++-- plugins/azure-devops/package.json | 14 ++++----- plugins/badges-backend/package.json | 8 ++--- plugins/badges/package.json | 14 ++++----- plugins/bazaar-backend/package.json | 6 ++-- plugins/bazaar/CHANGELOG.md | 14 +++++++++ plugins/bazaar/package.json | 16 +++++----- plugins/bitrise/package.json | 12 +++---- .../catalog-backend-module-ldap/CHANGELOG.md | 10 ++++++ .../catalog-backend-module-ldap/package.json | 10 +++--- .../CHANGELOG.md | 9 ++++++ .../package.json | 10 +++--- plugins/catalog-backend/CHANGELOG.md | 14 +++++++++ plugins/catalog-backend/package.json | 12 +++---- plugins/catalog-graph/CHANGELOG.md | 11 +++++++ plugins/catalog-graph/package.json | 14 ++++----- plugins/catalog-graphql/package.json | 4 +-- plugins/catalog-import/CHANGELOG.md | 12 +++++++ plugins/catalog-import/package.json | 16 +++++----- plugins/catalog-react/CHANGELOG.md | 12 +++++++ plugins/catalog-react/package.json | 14 ++++----- plugins/catalog/CHANGELOG.md | 12 +++++++ plugins/catalog/package.json | 16 +++++----- plugins/circleci/package.json | 12 +++---- plugins/cloudbuild/package.json | 12 +++---- plugins/code-coverage-backend/CHANGELOG.md | 10 ++++++ plugins/code-coverage-backend/package.json | 10 +++--- plugins/code-coverage/package.json | 14 ++++----- plugins/config-schema/package.json | 10 +++--- plugins/cost-insights/package.json | 8 ++--- plugins/explore-react/package.json | 4 +-- plugins/explore/package.json | 12 +++---- plugins/firehydrant/package.json | 10 +++--- plugins/fossa/CHANGELOG.md | 12 +++++++ plugins/fossa/package.json | 16 +++++----- plugins/gcp-projects/package.json | 8 ++--- plugins/git-release-manager/CHANGELOG.md | 9 ++++++ plugins/git-release-manager/package.json | 10 +++--- plugins/github-actions/package.json | 12 +++---- plugins/github-deployments/package.json | 14 ++++----- plugins/gitops-profiles/package.json | 8 ++--- plugins/graphiql/package.json | 8 ++--- plugins/graphql-backend/package.json | 4 +-- plugins/home/package.json | 8 ++--- plugins/ilert/package.json | 14 ++++----- plugins/jenkins-backend/package.json | 6 ++-- plugins/jenkins/CHANGELOG.md | 11 +++++++ plugins/jenkins/package.json | 14 ++++----- plugins/kafka-backend/package.json | 8 ++--- plugins/kafka/package.json | 12 +++---- plugins/kubernetes-backend/CHANGELOG.md | 12 +++++++ plugins/kubernetes-backend/package.json | 10 +++--- plugins/kubernetes-common/package.json | 4 +-- plugins/kubernetes/package.json | 12 +++---- plugins/lighthouse/package.json | 12 +++---- plugins/newrelic/package.json | 8 ++--- plugins/org/package.json | 12 +++---- plugins/pagerduty/package.json | 12 +++---- plugins/proxy-backend/package.json | 4 +-- plugins/rollbar-backend/package.json | 4 +-- plugins/rollbar/package.json | 12 +++---- .../package.json | 8 ++--- .../package.json | 8 ++--- plugins/scaffolder-backend/CHANGELOG.md | 12 +++++++ plugins/scaffolder-backend/package.json | 12 +++---- plugins/scaffolder-common/package.json | 4 +-- plugins/scaffolder/package.json | 14 ++++----- .../package.json | 4 +-- plugins/search-backend-module-pg/package.json | 6 ++-- plugins/search-backend-node/package.json | 4 +-- plugins/search-backend/package.json | 4 +-- plugins/search/package.json | 14 ++++----- plugins/sentry/CHANGELOG.md | 11 +++++++ plugins/sentry/package.json | 14 ++++----- plugins/shortcuts/package.json | 8 ++--- plugins/sonarqube/package.json | 12 +++---- plugins/splunk-on-call/package.json | 12 +++---- plugins/tech-radar/package.json | 8 ++--- plugins/techdocs-backend/CHANGELOG.md | 12 +++++++ plugins/techdocs-backend/package.json | 12 +++---- plugins/techdocs/CHANGELOG.md | 20 ++++++++++++ plugins/techdocs/package.json | 18 +++++------ plugins/todo-backend/package.json | 8 ++--- plugins/todo/package.json | 14 ++++----- plugins/user-settings/CHANGELOG.md | 15 +++++++++ plugins/user-settings/package.json | 12 +++---- plugins/xcmetrics/package.json | 10 +++--- 166 files changed, 898 insertions(+), 706 deletions(-) delete mode 100644 .changeset/blue-dingos-repeat.md delete mode 100644 .changeset/brave-spoons-grow.md delete mode 100644 .changeset/brave-waves-explain.md delete mode 100644 .changeset/brown-paws-marry.md delete mode 100644 .changeset/chilled-tools-worry.md delete mode 100644 .changeset/eighty-forks-fry.md delete mode 100644 .changeset/eleven-brooms-add.md delete mode 100644 .changeset/fair-jeans-heal.md delete mode 100644 .changeset/few-fishes-heal.md delete mode 100644 .changeset/four-days-sneeze.md delete mode 100644 .changeset/giant-phones-exercise.md delete mode 100644 .changeset/hip-pens-love.md delete mode 100644 .changeset/hot-wolves-share.md delete mode 100644 .changeset/khaki-pants-vanish.md delete mode 100644 .changeset/large-dragons-design.md delete mode 100644 .changeset/lucky-plums-brake.md delete mode 100644 .changeset/lucky-pumas-cross.md delete mode 100644 .changeset/mean-glasses-play.md delete mode 100644 .changeset/moody-bats-matter.md delete mode 100644 .changeset/nice-pillows-move.md delete mode 100644 .changeset/polite-eyes-punch.md delete mode 100644 .changeset/pretty-ways-search.md delete mode 100644 .changeset/quick-frogs-double.md delete mode 100644 .changeset/quiet-parrots-occur.md delete mode 100644 .changeset/rare-cooks-play.md delete mode 100644 .changeset/real-mice-beg.md delete mode 100644 .changeset/rude-comics-float.md delete mode 100644 .changeset/selfish-papayas-watch.md delete mode 100644 .changeset/sharp-crabs-stare.md delete mode 100644 .changeset/swift-pugs-serve.md delete mode 100644 .changeset/techdocs-dull-glasses-decide.md delete mode 100644 .changeset/tiny-panthers-jump.md delete mode 100644 .changeset/twelve-candles-mix.md delete mode 100644 .changeset/two-ties-eat.md delete mode 100644 .changeset/wise-camels-run.md delete mode 100644 .changeset/wise-hats-remember.md create mode 100644 plugins/bazaar/CHANGELOG.md diff --git a/.changeset/blue-dingos-repeat.md b/.changeset/blue-dingos-repeat.md deleted file mode 100644 index 111a94a00b..0000000000 --- a/.changeset/blue-dingos-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Require that audience URLs for Okta authentication start with https diff --git a/.changeset/brave-spoons-grow.md b/.changeset/brave-spoons-grow.md deleted file mode 100644 index d1bd166510..0000000000 --- a/.changeset/brave-spoons-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Switched to relying on the built-in support for async loading in `react-syntax-highlighter`. This should provide further improvements to async rendering and lazy loading, and avoid test flakiness that was happening because of the significant number or resources being loaded in lazily all at once. diff --git a/.changeset/brave-waves-explain.md b/.changeset/brave-waves-explain.md deleted file mode 100644 index 5e76f192e0..0000000000 --- a/.changeset/brave-waves-explain.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Removed the included `jest` configuration from the root `package.json` as the `transformModules` option no longer exists. - -To apply this change to an existing app, make the follow change to the root `package.json`: - -```diff -- "jest": { -- "transformModules": [ -- "@asyncapi/react-component" -- ] -- } -``` diff --git a/.changeset/brown-paws-marry.md b/.changeset/brown-paws-marry.md deleted file mode 100644 index c771d5e1f4..0000000000 --- a/.changeset/brown-paws-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Fix a bug preventing an access token to be refreshed a second time with the GitHub provider. diff --git a/.changeset/chilled-tools-worry.md b/.changeset/chilled-tools-worry.md deleted file mode 100644 index 58f89ef503..0000000000 --- a/.changeset/chilled-tools-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Configuration schema errors are now filtered using the provided visibility option. This means that schema errors due to missing backend configuration will no longer break frontend builds. diff --git a/.changeset/eighty-forks-fry.md b/.changeset/eighty-forks-fry.md deleted file mode 100644 index 4a25dd128a..0000000000 --- a/.changeset/eighty-forks-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Use docs/README.md or README.md as fallback if docs/index.md is missing diff --git a/.changeset/eleven-brooms-add.md b/.changeset/eleven-brooms-add.md deleted file mode 100644 index 4554196ba8..0000000000 --- a/.changeset/eleven-brooms-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Fix Kubernetes plugin custom objects lookup regression diff --git a/.changeset/fair-jeans-heal.md b/.changeset/fair-jeans-heal.md deleted file mode 100644 index 48c0adbfc2..0000000000 --- a/.changeset/fair-jeans-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Allow `catalog:register` action to register optional locations diff --git a/.changeset/few-fishes-heal.md b/.changeset/few-fishes-heal.md deleted file mode 100644 index 9fc9e0d17f..0000000000 --- a/.changeset/few-fishes-heal.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Add `` as an alternative to `` that -shows a grid of card instead of table. - -Extend `` to display the entity title of the entity instead of the -name if available. diff --git a/.changeset/four-days-sneeze.md b/.changeset/four-days-sneeze.md deleted file mode 100644 index 93517d3a73..0000000000 --- a/.changeset/four-days-sneeze.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Display entity title (if defined) in titles of TechDocs search results diff --git a/.changeset/giant-phones-exercise.md b/.changeset/giant-phones-exercise.md deleted file mode 100644 index b190c2e7f0..0000000000 --- a/.changeset/giant-phones-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-fossa': patch ---- - -Add `entitiesFilter` option to `FossaPage` to filter entities displayed in the table. diff --git a/.changeset/hip-pens-love.md b/.changeset/hip-pens-love.md deleted file mode 100644 index d1549789bf..0000000000 --- a/.changeset/hip-pens-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Take CatalogParser in account when processing file locations. diff --git a/.changeset/hot-wolves-share.md b/.changeset/hot-wolves-share.md deleted file mode 100644 index a6294b8c9c..0000000000 --- a/.changeset/hot-wolves-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Minor rearrangement of `Stitcher` to clarify the scope of one stitch round diff --git a/.changeset/khaki-pants-vanish.md b/.changeset/khaki-pants-vanish.md deleted file mode 100644 index df58365e28..0000000000 --- a/.changeset/khaki-pants-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-bazaar': patch ---- - -Bumped plugin-catalog-react version from 0.5.0 to 0.6.0 diff --git a/.changeset/large-dragons-design.md b/.changeset/large-dragons-design.md deleted file mode 100644 index e1d5a913fe..0000000000 --- a/.changeset/large-dragons-design.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -fix: parameter name on call to sentry api diff --git a/.changeset/lucky-plums-brake.md b/.changeset/lucky-plums-brake.md deleted file mode 100644 index 80eb7b4cb4..0000000000 --- a/.changeset/lucky-plums-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Align grid height diff --git a/.changeset/lucky-pumas-cross.md b/.changeset/lucky-pumas-cross.md deleted file mode 100644 index 4a8e0c5f2f..0000000000 --- a/.changeset/lucky-pumas-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Fixed invalid wrap in DismissableBanner component diff --git a/.changeset/mean-glasses-play.md b/.changeset/mean-glasses-play.md deleted file mode 100644 index e8a0337843..0000000000 --- a/.changeset/mean-glasses-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Add "rate limit exceeded" to error from GithubUrlReader if that is the cause of a read failure diff --git a/.changeset/moody-bats-matter.md b/.changeset/moody-bats-matter.md deleted file mode 100644 index a2c974cd09..0000000000 --- a/.changeset/moody-bats-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': minor ---- - -Removed the `EnvFunc` public export. Its only usage was to be passed in to `LoadConfigOptions.experimentalEnvFunc`. If you were using this type, add a definition in your own project instead with the signature `(name: string) => Promise`. diff --git a/.changeset/nice-pillows-move.md b/.changeset/nice-pillows-move.md deleted file mode 100644 index 5649096d10..0000000000 --- a/.changeset/nice-pillows-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-azure-devops-backend': patch ---- - -Re-exported types from azure-devops-node-api in @backstage/plugin-azure-devops-backend. diff --git a/.changeset/polite-eyes-punch.md b/.changeset/polite-eyes-punch.md deleted file mode 100644 index fed1116bb2..0000000000 --- a/.changeset/polite-eyes-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -add caData support for kubernetes client config diff --git a/.changeset/pretty-ways-search.md b/.changeset/pretty-ways-search.md deleted file mode 100644 index 212c23b09a..0000000000 --- a/.changeset/pretty-ways-search.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Add Firestore as key-store provider. -Add `auth.keyStore` section to application config. diff --git a/.changeset/quick-frogs-double.md b/.changeset/quick-frogs-double.md deleted file mode 100644 index c083abe609..0000000000 --- a/.changeset/quick-frogs-double.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/config-loader': patch -'@backstage/errors': patch ---- - -Add public tags and documentation diff --git a/.changeset/quiet-parrots-occur.md b/.changeset/quiet-parrots-occur.md deleted file mode 100644 index c9a6ce9a1d..0000000000 --- a/.changeset/quiet-parrots-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-graph': patch ---- - -Capture analytics events for clicks in the graph. diff --git a/.changeset/rare-cooks-play.md b/.changeset/rare-cooks-play.md deleted file mode 100644 index b76d34ee62..0000000000 --- a/.changeset/rare-cooks-play.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/create-app': minor ---- - -Removed `@backstage/plugin-welcome`, no new updates to the packages will be -published in the future. - -The welcome plugin was used by early alpha versions of Backstage, but today only -contained a simple page with welcome instructions. It was superseded by -`@backstage/plugin-home` which can be used to build a homepage customized to the -needs of your organization. - -If it's still used in your app, remove the dependency from your `package.json` -as well as left over code. diff --git a/.changeset/real-mice-beg.md b/.changeset/real-mice-beg.md deleted file mode 100644 index 7dde97f6c2..0000000000 --- a/.changeset/real-mice-beg.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/errors': patch ---- - -Two new helpers have been added that make it easier to migrate to considering thrown errors to be of the type `unknown` in TypeScript. The helpers are `assertError` and `isError`, and can be called to make sure that an unknown value conforms to the shape of an `ErrorLike` object. The `assertError` function is a type-guard that throws in the case of a mismatch, while `isError` returns false. - -A new error constructor has also been added, `ForwardedError`, which can be used to add context to a forwarded error. It requires both a message and a cause, and inherits the `name` property from the `cause`. diff --git a/.changeset/rude-comics-float.md b/.changeset/rude-comics-float.md deleted file mode 100644 index cb3a883fa1..0000000000 --- a/.changeset/rude-comics-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins': patch ---- - -Sort Jenkins build by timestamp in `CITable` diff --git a/.changeset/selfish-papayas-watch.md b/.changeset/selfish-papayas-watch.md deleted file mode 100644 index 1d9b8a1ab6..0000000000 --- a/.changeset/selfish-papayas-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-git-release-manager': patch ---- - -Minor internal type improvement diff --git a/.changeset/sharp-crabs-stare.md b/.changeset/sharp-crabs-stare.md deleted file mode 100644 index a81e83686f..0000000000 --- a/.changeset/sharp-crabs-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Add a `GitHubOrgEntityProvider` that can be used instead of the `GithubOrgReaderProcessor`. diff --git a/.changeset/swift-pugs-serve.md b/.changeset/swift-pugs-serve.md deleted file mode 100644 index 8262a66d7f..0000000000 --- a/.changeset/swift-pugs-serve.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-user-settings': patch ---- - -Atlassian auth provider - -- AtlassianAuth added to core-app-api -- Atlassian provider added to plugin-auth-backend -- Updated user-settings with Atlassian connection diff --git a/.changeset/techdocs-dull-glasses-decide.md b/.changeset/techdocs-dull-glasses-decide.md deleted file mode 100644 index f043c2f607..0000000000 --- a/.changeset/techdocs-dull-glasses-decide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Only replace the shadow dom if the content is changed to avoid a flickering UI. diff --git a/.changeset/tiny-panthers-jump.md b/.changeset/tiny-panthers-jump.md deleted file mode 100644 index d059b826da..0000000000 --- a/.changeset/tiny-panthers-jump.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/cli': patch -'@backstage/config-loader': patch -'@backstage/core-app-api': patch -'@backstage/core-components': patch -'@backstage/core-plugin-api': patch -'@backstage/create-app': patch -'@backstage/techdocs-common': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Internal updates to apply more strict checks to throw errors. diff --git a/.changeset/twelve-candles-mix.md b/.changeset/twelve-candles-mix.md deleted file mode 100644 index 3218b2997c..0000000000 --- a/.changeset/twelve-candles-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Use entity title (if defined) as title of documents indexed by `DefaultCatalogCollator` diff --git a/.changeset/two-ties-eat.md b/.changeset/two-ties-eat.md deleted file mode 100644 index 69edfb16c8..0000000000 --- a/.changeset/two-ties-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Improved documentation for exported symbols. diff --git a/.changeset/wise-camels-run.md b/.changeset/wise-camels-run.md deleted file mode 100644 index 4438032b77..0000000000 --- a/.changeset/wise-camels-run.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@backstage/cli': minor ---- - -The Jest configuration that's included with the Backstage CLI has received several changes. - -As a part of migrating to more widespread usage of ESM modules, the default configuration now transforms all source files everywhere, including those within `node_modules`. Due to this change the existing `transformModules` option has been removed and will be ignored. There is also a list of known packages that do not require transforms in the CLI, which will evolve over time. If needed there will also be an option to add packages to this list in the future, but it is not included yet to avoid clutter. - -To counteract the slowdown of the additional transforms that have been introduced, the default configuration has also been reworked to enable caching across different packages. Previously each package in a Backstage monorepo would have its own isolated Jest cache, but it is now shared between packages that have a similar enough Jest configuration. - -Another change that will speed up test execution is that the transformer for `.esm.js` files has been switched. It used to be an ESM transformer based on Babel, but it is also done by sucrase now since it is significantly faster. - -The changes above are not strictly breaking as all tests should still work. It may however cause excessive slowdowns in projects that have configured custom transforms in the `jest` field within `package.json` files. In this case it is either best to consider removing the custom transforms, or overriding the `transformIgnorePatterns` to instead use Jest's default `'/node_modules/'` pattern. - -This change also removes the `@backstage/cli/config/jestEsmTransform.js` transform, which can be replaced by using the `@backstage/cli/config/sucraseEsmTransform.js` transform instead. diff --git a/.changeset/wise-hats-remember.md b/.changeset/wise-hats-remember.md deleted file mode 100644 index 6666502bee..0000000000 --- a/.changeset/wise-hats-remember.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Add `MicrosoftGraphOrgEntityProvider` as an alternative to `MicrosoftGraphOrgReaderProcessor` that automatically handles user and group deletions. diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index d81623e7c0..1dee41af56 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,24 @@ # example-app +## 0.2.51 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/plugin-techdocs@0.12.3 + - @backstage/plugin-sentry@0.3.26 + - @backstage/plugin-user-settings@0.3.10 + - @backstage/plugin-catalog-graph@0.2.1 + - @backstage/plugin-jenkins@0.5.11 + - @backstage/core-app-api@0.1.18 + - @backstage/core-plugin-api@0.1.11 + - @backstage/cli@0.8.0 + - @backstage/plugin-catalog@0.7.2 + - @backstage/plugin-catalog-import@0.7.3 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.2.50 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 3f4a8bef70..833e146d45 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,22 +1,22 @@ { "name": "example-app", - "version": "0.2.50", + "version": "0.2.51", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/catalog-model": "^0.9.5", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/integration-react": "^0.1.12", "@backstage/plugin-api-docs": "^0.6.12", "@backstage/plugin-azure-devops": "^0.1.1", "@backstage/plugin-badges": "^0.2.13", - "@backstage/plugin-catalog": "^0.7.1", - "@backstage/plugin-catalog-graph": "^0.2.0", - "@backstage/plugin-catalog-import": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog": "^0.7.2", + "@backstage/plugin-catalog-graph": "^0.2.1", + "@backstage/plugin-catalog-import": "^0.7.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/plugin-circleci": "^0.2.27", "@backstage/plugin-cloudbuild": "^0.2.27", "@backstage/plugin-code-coverage": "^0.1.15", @@ -26,7 +26,7 @@ "@backstage/plugin-github-actions": "^0.4.22", "@backstage/plugin-graphiql": "^0.2.20", "@backstage/plugin-home": "^0.4.4", - "@backstage/plugin-jenkins": "^0.5.10", + "@backstage/plugin-jenkins": "^0.5.11", "@backstage/plugin-kafka": "^0.2.19", "@backstage/plugin-kubernetes": "^0.4.17", "@backstage/plugin-lighthouse": "^0.2.29", @@ -36,12 +36,12 @@ "@backstage/plugin-rollbar": "^0.3.18", "@backstage/plugin-scaffolder": "^0.11.8", "@backstage/plugin-search": "^0.4.15", - "@backstage/plugin-sentry": "^0.3.25", + "@backstage/plugin-sentry": "^0.3.26", "@backstage/plugin-shortcuts": "^0.1.12", "@backstage/plugin-tech-radar": "^0.4.11", - "@backstage/plugin-techdocs": "^0.12.2", + "@backstage/plugin-techdocs": "^0.12.3", "@backstage/plugin-todo": "^0.1.14", - "@backstage/plugin-user-settings": "^0.3.9", + "@backstage/plugin-user-settings": "^0.3.10", "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index e7b897692e..131b160a36 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/backend-common +## 0.9.7 + +### Patch Changes + +- be59619212: Add "rate limit exceeded" to error from GithubUrlReader if that is the cause of a read failure +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/config-loader@0.7.0 + - @backstage/errors@0.1.3 + ## 0.9.6 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 16167f7803..8da4184f71 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.6", + "version": "0.9.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,8 +31,8 @@ "dependencies": { "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.6.10", - "@backstage/errors": "^0.1.2", + "@backstage/config-loader": "^0.7.0", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.7", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", @@ -77,7 +77,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.18", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 6e10d188df..287e40a296 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-test-utils +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/cli@0.8.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 1713f05d25..2f29c2412b 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.2", - "@backstage/cli": "^0.7.11", + "@backstage/backend-common": "^0.9.7", + "@backstage/cli": "^0.8.0", "@backstage/config": "^0.1.9", "knex": "^0.95.1", "mysql2": "^2.2.5", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.8.0", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/package.json b/packages/backend/package.json index d02282d569..b9a28c2853 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -24,36 +24,36 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", "@backstage/integration": "^0.6.8", - "@backstage/plugin-app-backend": "^0.3.16", - "@backstage/plugin-auth-backend": "^0.4.4", - "@backstage/plugin-azure-devops-backend": "^0.1.2", + "@backstage/plugin-app-backend": "^0.3.17", + "@backstage/plugin-auth-backend": "^0.4.5", + "@backstage/plugin-azure-devops-backend": "^0.1.3", "@backstage/plugin-badges-backend": "^0.1.11", - "@backstage/plugin-catalog-backend": "^0.17.0", - "@backstage/plugin-code-coverage-backend": "^0.1.13", + "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/plugin-code-coverage-backend": "^0.1.14", "@backstage/plugin-graphql-backend": "^0.1.9", "@backstage/plugin-jenkins-backend": "^0.1.6", - "@backstage/plugin-kubernetes-backend": "^0.3.17", + "@backstage/plugin-kubernetes-backend": "^0.3.18", "@backstage/plugin-kafka-backend": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.13", "@backstage/plugin-rollbar-backend": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.15.8", + "@backstage/plugin-scaffolder-backend": "^0.15.10", "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", "@backstage/plugin-search-backend": "^0.2.6", "@backstage/plugin-search-backend-node": "^0.4.2", "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4", "@backstage/plugin-search-backend-module-pg": "^0.2.1", - "@backstage/plugin-techdocs-backend": "^0.10.4", + "@backstage/plugin-techdocs-backend": "^0.10.5", "@backstage/plugin-todo-backend": "^0.1.13", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.2.1", - "example-app": "^0.2.50", + "example-app": "^0.2.51", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -65,7 +65,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 8da62f6115..f8dfd59e41 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/errors": "^0.1.2", + "@backstage/catalog-model": "^0.9.5", + "@backstage/errors": "^0.1.3", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "msw": "^0.29.0" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index e7c36953db..066b01cd50 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-model +## 0.9.5 + +### Patch Changes + +- ab2df3be33: Improved documentation for exported symbols. +- Updated dependencies + - @backstage/errors@0.1.3 + ## 0.9.4 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 45b947c890..69d1c34ba9 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.4", + "version": "0.9.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.3", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.13", "ajv": "^7.0.3", @@ -41,7 +41,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index fce803b3cc..7cb3c5be91 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/cli +## 0.8.0 + +### Minor Changes + +- b486adb8c6: The Jest configuration that's included with the Backstage CLI has received several changes. + + As a part of migrating to more widespread usage of ESM modules, the default configuration now transforms all source files everywhere, including those within `node_modules`. Due to this change the existing `transformModules` option has been removed and will be ignored. There is also a list of known packages that do not require transforms in the CLI, which will evolve over time. If needed there will also be an option to add packages to this list in the future, but it is not included yet to avoid clutter. + + To counteract the slowdown of the additional transforms that have been introduced, the default configuration has also been reworked to enable caching across different packages. Previously each package in a Backstage monorepo would have its own isolated Jest cache, but it is now shared between packages that have a similar enough Jest configuration. + + Another change that will speed up test execution is that the transformer for `.esm.js` files has been switched. It used to be an ESM transformer based on Babel, but it is also done by sucrase now since it is significantly faster. + + The changes above are not strictly breaking as all tests should still work. It may however cause excessive slowdowns in projects that have configured custom transforms in the `jest` field within `package.json` files. In this case it is either best to consider removing the custom transforms, or overriding the `transformIgnorePatterns` to instead use Jest's default `'/node_modules/'` pattern. + + This change also removes the `@backstage/cli/config/jestEsmTransform.js` transform, which can be replaced by using the `@backstage/cli/config/sucraseEsmTransform.js` transform instead. + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/config-loader@0.7.0 + - @backstage/errors@0.1.3 + ## 0.7.16 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index ebd8f0169c..da9e161fef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.7.16", + "version": "0.8.0", "private": false, "publishConfig": { "access": "public" @@ -30,8 +30,8 @@ "dependencies": { "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.6.10", - "@backstage/errors": "^0.1.2", + "@backstage/config-loader": "^0.7.0", + "@backstage/errors": "^0.1.3", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", @@ -116,11 +116,11 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/core-app-api": "^0.1.17", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@backstage/theme": "^0.2.11", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index b22d5c311c..7aabaef583 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/codemods +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-app-api@0.1.18 + - @backstage/core-plugin-api@0.1.11 + ## 0.1.18 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 47b97edbe8..77bdd181be 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.18", + "version": "0.1.19", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 34fc34b441..0b4ef9a5f3 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/config-loader +## 0.7.0 + +### Minor Changes + +- 7e97d0b8c1: Removed the `EnvFunc` public export. Its only usage was to be passed in to `LoadConfigOptions.experimentalEnvFunc`. If you were using this type, add a definition in your own project instead with the signature `(name: string) => Promise`. + +### Patch Changes + +- 223e8de6b4: Configuration schema errors are now filtered using the provided visibility option. This means that schema errors due to missing backend configuration will no longer break frontend builds. +- 7e97d0b8c1: Add public tags and documentation +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/errors@0.1.3 + ## 0.6.10 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index ebca3ffdf3..03ea61abe2 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.6.10", + "version": "0.7.0", "private": false, "publishConfig": { "access": "public", @@ -32,7 +32,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.9", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.3", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "chokidar": "^3.5.2", diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 65fd736283..e01e89f04c 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/core-app-api +## 0.1.18 + +### Patch Changes + +- 202f322927: Atlassian auth provider + + - AtlassianAuth added to core-app-api + - Atlassian provider added to plugin-auth-backend + - Updated user-settings with Atlassian connection + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + ## 0.1.17 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 3000b0e01e..d11b7122ce 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.17", + "version": "0.1.18", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", + "@backstage/core-components": "^0.7.1", "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@backstage/test-utils-core": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 4300acb628..375f23dfda 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-components +## 0.7.1 + +### Patch Changes + +- e535ea766a: Switched to relying on the built-in support for async loading in `react-syntax-highlighter`. This should provide further improvements to async rendering and lazy loading, and avoid test flakiness that was happening because of the significant number or resources being loaded in lazily all at once. +- 2023a9683f: Fixed invalid wrap in DismissableBanner component +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + ## 0.7.0 ### Minor Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index e637bc1b11..865f5b6df9 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.7.0", + "version": "0.7.1", "private": false, "publishConfig": { "access": "public", @@ -30,8 +30,8 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.11", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", @@ -67,8 +67,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.17", - "@backstage/cli": "^0.7.16", + "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 8982002abc..21ce590e1c 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/core-plugin-api +## 0.1.11 + +### Patch Changes + +- 202f322927: Atlassian auth provider + + - AtlassianAuth added to core-app-api + - Atlassian provider added to plugin-auth-backend + - Updated user-settings with Atlassian connection + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. + ## 0.1.10 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 5c8daec606..471e510d88 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.1.10", + "version": "0.1.11", "private": false, "publishConfig": { "access": "public", @@ -42,8 +42,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.15", - "@backstage/core-app-api": "^0.1.16", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/test-utils": "^0.1.18", "@backstage/test-utils-core": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index fb118cd0d5..070d558124 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/create-app +## 0.4.0 + +### Minor Changes + +- 5914668655: Removed `@backstage/plugin-welcome`, no new updates to the packages will be + published in the future. + + The welcome plugin was used by early alpha versions of Backstage, but today only + contained a simple page with welcome instructions. It was superseded by + `@backstage/plugin-home` which can be used to build a homepage customized to the + needs of your organization. + + If it's still used in your app, remove the dependency from your `package.json` + as well as left over code. + +### Patch Changes + +- b486adb8c6: Removed the included `jest` configuration from the root `package.json` as the `transformModules` option no longer exists. + + To apply this change to an existing app, make the follow change to the root `package.json`: + + ```diff + - "jest": { + - "transformModules": [ + - "@asyncapi/react-component" + - ] + - } + ``` + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. + ## 0.3.45 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 87ccee4d92..6cf606ed49 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.3.45", + "version": "0.4.0", "private": false, "publishConfig": { "access": "public" diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 13fba4156a..e01390e169 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.17", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.9", - "@backstage/catalog-model": "^0.9.3", + "@backstage/core-app-api": "^0.1.18", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/catalog-model": "^0.9.5", "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/test-utils": "^0.1.19", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -50,7 +50,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index 7cadaa6e07..368fa26593 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/errors +## 0.1.3 + +### Patch Changes + +- 7e97d0b8c1: Add public tags and documentation +- 6077d61e73: Two new helpers have been added that make it easier to migrate to considering thrown errors to be of the type `unknown` in TypeScript. The helpers are `assertError` and `isError`, and can be called to make sure that an unknown value conforms to the shape of an `ErrorLike` object. The `assertError` function is a type-guard that throws in the case of a mismatch, while `isError` returns false. + + A new error constructor has also been added, `ForwardedError`, which can be used to add context to a forwarded error. It requires both a message and a cause, and inherits the `name` property from the `cause`. + ## 0.1.2 ### Patch Changes diff --git a/packages/errors/package.json b/packages/errors/package.json index a9911ce88f..e3d49f55c7 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/errors", "description": "Common utilities for error handling within Backstage", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 19db6f1d99..b0867c755b 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.9", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/integration": "^0.6.8", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -34,7 +34,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/integration/package.json b/packages/integration/package.json index 45ca6d23d8..5dda924580 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -39,8 +39,8 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/config-loader": "^0.6.10", + "@backstage/cli": "^0.8.0", + "@backstage/config-loader": "^0.7.0", "@backstage/test-utils": "^0.1.19", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", diff --git a/packages/search-common/package.json b/packages/search-common/package.json index be1aad16b0..46e3518b51 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -39,7 +39,7 @@ "@backstage/config": "^0.1.6" }, "devDependencies": { - "@backstage/cli": "^0.7.10" + "@backstage/cli": "^0.8.0" }, "jest": { "roots": [ diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 7f46d8d8b8..fa32a0355e 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/techdocs-common +## 0.10.4 + +### Patch Changes + +- 87f5b9db13: Use docs/README.md or README.md as fallback if docs/index.md is missing +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.10.3 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index cbcdc04569..fc3a9376dc 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.10.3", + "version": "0.10.4", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,10 +38,10 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.6", - "@backstage/catalog-model": "^0.9.4", + "@backstage/backend-common": "^0.9.7", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@backstage/search-common": "^0.2.0", "@backstage/integration": "^0.6.7", "@google-cloud/storage": "^5.6.0", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index cd79f1e62d..22dc950bd4 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.17", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-app-api": "^0.1.18", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/test-utils-core": "^0.1.3", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -46,7 +46,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/theme/package.json b/packages/theme/package.json index 3da99337ec..3bc209a782 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.7.16" + "@backstage/cli": "^0.8.0" }, "files": [ "dist" diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 0be6f0cacd..7cc0b56a34 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -33,7 +33,7 @@ "react": "^16.12.0" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.8.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2" diff --git a/plugins/allure/package.json b/plugins/allure/package.json index a12d0436c2..26a5b4cdeb 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,8 +36,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 7c63a24c91..0f9759bf4f 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -34,8 +34,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index c9f92fee15..78ce0c7ed6 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -31,11 +31,11 @@ }, "dependencies": { "@asyncapi/react-component": "^0.23.0", - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,8 +53,8 @@ "swagger-client": "3.16.1" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 9d71990ba0..1424936280 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-backend +## 0.3.17 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@0.7.0 + - @backstage/backend-common@0.9.7 + ## 0.3.16 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 3b07366de3..6047b5aeb1 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.16", + "version": "0.3.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/config-loader": "^0.6.5", + "@backstage/backend-common": "^0.9.7", + "@backstage/config-loader": "^0.7.0", "@backstage/config": "^0.1.8", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -41,7 +41,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.4", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index ab24c3670c..f8896c95ba 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-auth-backend +## 0.4.5 + +### Patch Changes + +- 9322e632e9: Require that audience URLs for Okta authentication start with https +- de3e26aecc: Fix a bug preventing an access token to be refreshed a second time with the GitHub provider. +- ab9b4a6ea6: Add Firestore as key-store provider. + Add `auth.keyStore` section to application config. +- 202f322927: Atlassian auth provider + + - AtlassianAuth added to core-app-api + - Atlassian provider added to plugin-auth-backend + - Updated user-settings with Atlassian connection + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.4.4 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 8f7f623652..d593db2aa5 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.4.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@backstage/test-utils": "^0.1.19", "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", @@ -73,7 +73,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 869bcb6e2f..a776aea332 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-azure-devops-backend +## 0.1.3 + +### Patch Changes + +- f67dff0d20: Re-exported types from azure-devops-node-api in @backstage/plugin-azure-devops-backend. +- Updated dependencies + - @backstage/backend-common@0.9.7 + ## 0.1.2 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 52f5f86d29..ec2bc84627 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.5", + "@backstage/backend-common": "^0.9.7", "@backstage/config": "^0.1.9", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.29.0" diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index c4aea032d2..38f3216c7b 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -27,11 +27,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.3", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index e38b502a49..51a181612a 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -31,11 +31,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@types/express": "^4.17.6", "badge-maker": "^3.3.0", "cors": "^2.8.5", @@ -46,7 +46,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 541f910f53..5bc2f3e519 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -27,11 +27,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 82c3317555..a9ac0153b8 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.2", - "@backstage/backend-test-utils": "^0.1.7", + "@backstage/backend-common": "^0.9.7", + "@backstage/backend-test-utils": "^0.1.8", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.6" + "@backstage/cli": "^0.8.0" }, "files": [ "dist", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md new file mode 100644 index 0000000000..3cd13cfa8a --- /dev/null +++ b/plugins/bazaar/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-bazaar + +## 0.1.2 + +### Patch Changes + +- 5e43a73dd4: Bumped plugin-catalog-react version from 0.5.0 to 0.6.0 +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + - @backstage/cli@0.8.0 + - @backstage/plugin-catalog@0.7.2 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 98349a5724..a5dc9f7b1e 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,12 +21,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/cli": "^0.7.16", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/cli": "^0.8.0", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.6.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,7 +39,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@backstage/dev-utils": "^0.2.12", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index cfaa081f49..9123188947 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,8 +39,8 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 849ca9309e..6bac00a7a3 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.5 + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.1 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.3.4 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 9884a55bcc..7236e780c4 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend modules that helps integrate towards LDAP", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,17 +29,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.2", - "@backstage/plugin-catalog-backend": "^0.17.0", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-backend": "^0.17.1", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 749525abac..7f3b8657f9 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.8 + +### Patch Changes + +- 406dcf06e5: Add `MicrosoftGraphOrgEntityProvider` as an alternative to `MicrosoftGraphOrgReaderProcessor` that automatically handles user and group deletions. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.1 + - @backstage/catalog-model@0.9.5 + ## 0.2.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 3e0a83a396..d0f664f428 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/plugin-catalog-backend": "^0.17.0", + "@backstage/plugin-catalog-backend": "^0.17.1", "@microsoft/microsoft-graph-types": "^2.6.0", "cross-fetch": "^3.0.6", "lodash": "^4.17.21", @@ -41,8 +41,8 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.9.6", - "@backstage/cli": "^0.7.16", + "@backstage/backend-common": "^0.9.7", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@types/lodash": "^4.14.151", "msw": "^0.29.0" diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 4aac915289..b098270431 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend +## 0.17.1 + +### Patch Changes + +- 3adaf88db2: Take CatalogParser in account when processing file locations. +- 1f62d1cbe9: Minor rearrangement of `Stitcher` to clarify the scope of one stitch round +- 3ba87f514e: Add a `GitHubOrgEntityProvider` that can be used instead of the `GithubOrgReaderProcessor`. +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- 177401b571: Use entity title (if defined) as title of documents indexed by `DefaultCatalogCollator` +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.17.0 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 4375687365..896bbde592 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.17.0", + "version": "0.17.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/search-common": "^0.2.0", "@octokit/graphql": "^4.5.8", @@ -61,8 +61,8 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.7", - "@backstage/cli": "^0.7.16", + "@backstage/backend-test-utils": "^0.1.8", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 79910921ca..a156be8fe0 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-graph +## 0.2.1 + +### Patch Changes + +- ce0aef1841: Capture analytics events for clicks in the graph. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.2.0 ### Minor Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 29fd6876e4..b0dca011ca 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -41,10 +41,10 @@ "p-limit": "^3.1.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", - "@backstage/core-app-api": "^0.1.17", + "@backstage/core-app-api": "^0.1.18", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index f0c8b3a9a8..80ad7f9a16 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.0", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.8", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -42,7 +42,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.11", "@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 4f903bd731..d06f38698b 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-import +## 0.7.3 + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.7.2 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index d443d3cb22..fe453ca45d 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.7.2", + "version": "0.7.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,13 +32,13 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -55,8 +55,8 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 3ea6c26ae0..26999f9e35 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-react +## 0.6.1 + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/errors@0.1.3 + - @backstage/core-app-api@0.1.18 + - @backstage/core-plugin-api@0.1.11 + - @backstage/catalog-model@0.9.5 + ## 0.6.0 ### Minor Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 2e9fa8fdd6..3373641d16 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.6.0", + "version": "0.6.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-app-api": "^0.1.17", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-app-api": "^0.1.18", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -50,7 +50,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 48c7c65c70..0030f702ec 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog +## 0.7.2 + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.7.1 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 45b9549538..0557a047e6 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "0.7.1", + "version": "0.7.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,12 +32,12 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.0", - "@backstage/errors": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.6.1", + "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,8 +51,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 58b3f5ffe6..8f49c4ac56 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -32,10 +32,10 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,8 +52,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 127143ddb2..6699fc1e00 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,8 +49,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 08a2a62e14..8e8f9ceac8 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-code-coverage-backend +## 0.1.14 + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.1.13 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index c059dd50c2..56be697bac 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.1.13", + "version": "0.1.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,11 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", @@ -37,7 +37,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.29.0", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 01d8c7258d..2d74ddd055 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -21,12 +21,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -41,8 +41,8 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 856d96940e..1b2214de8b 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -22,9 +22,9 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,8 +36,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index d424001e27..7eedd9a5bc 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,8 +55,8 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 08eb02683c..263137e93f 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -29,10 +29,10 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core-plugin-api": "^0.1.3" + "@backstage/core-plugin-api": "^0.1.11" }, "devDependencies": { - "@backstage/cli": "^0.7.2", + "@backstage/cli": "^0.8.0", "@backstage/dev-utils": "^0.2.0", "@backstage/test-utils": "^0.1.14", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 9ed480b91f..281d1727e8 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -31,10 +31,10 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/plugin-explore-react": "^0.0.6", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -49,8 +49,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index e199153805..e72bdfda51 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -22,9 +22,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,8 +35,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 04dd560e50..f422dfbe41 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-fossa +## 0.2.21 + +### Patch Changes + +- 09c106b96c: Add `entitiesFilter` option to `FossaPage` to filter entities displayed in the table. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.2.20 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index c7751b7002..c33e5f64ac 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.20", + "version": "0.2.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,8 +49,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index e346a313fb..0a3f70486e 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,8 +43,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 36eb386a9a..72bcf87b2a 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-git-release-manager +## 0.3.2 + +### Patch Changes + +- 82ea1f2bed: Minor internal type improvement +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + ## 0.3.1 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index d076f0312c..a63f491f48 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/integration": "^0.6.8", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -39,8 +39,8 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 19e8548ebb..7958a97cb8 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -33,11 +33,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,8 +52,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 5282ad19ea..ba226037ff 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -21,13 +21,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,8 +39,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index acd19cf681..9088c79fd9 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 8e31bb3225..1c5ebe17f6 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index e87bd52934..6f1f0000ea 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", + "@backstage/backend-common": "^0.9.7", "@backstage/config": "^0.1.8", "@backstage/plugin-catalog-graphql": "^0.2.12", "@graphql-modules/core": "^0.7.17", @@ -47,7 +47,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.29.0", diff --git a/plugins/home/package.json b/plugins/home/package.json index 9764748024..28b9fa9e66 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,8 +35,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index e19f77395c..34db6580fe 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@date-io/luxon": "2.x", "@material-ui/core": "^4.12.2", @@ -39,8 +39,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index b741cc3d11..bdae720c0e 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -22,9 +22,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.29.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index ed4ccd63a8..84db4728bc 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-jenkins +## 0.5.11 + +### Patch Changes + +- 3c756657b8: Sort Jenkins build by timestamp in `CITable` +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.5.10 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 5c5aa53010..fa6a5e6d52 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.5.10", + "version": "0.5.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 25b9e32679..e271decc8b 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.4", - "@backstage/catalog-model": "^0.9.3", + "@backstage/backend-common": "^0.9.7", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -44,7 +44,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.13", + "@backstage/cli": "^0.8.0", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 125530dcc2..6b91797359 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,8 +35,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 6b37417f72..d506571a3f 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-backend +## 0.3.18 + +### Patch Changes + +- b61c50a12f: Fix Kubernetes plugin custom objects lookup regression +- c57b075d18: add caData support for kubernetes client config +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.3.17 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index b2026faea6..be2903dff5 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.3.17", + "version": "0.3.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.4", - "@backstage/catalog-model": "^0.9.3", + "@backstage/backend-common": "^0.9.7", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.3", "@backstage/plugin-kubernetes-common": "^0.1.4", "@google-cloud/container": "^2.2.0", "@kubernetes/client-node": "^0.15.0", @@ -55,7 +55,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index ec3fec1c56..28dfc94c49 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -35,11 +35,11 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@kubernetes/client-node": "^0.15.0" }, "devDependencies": { - "@backstage/cli": "^0.7.15" + "@backstage/cli": "^0.8.0" }, "jest": { "roots": [ diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index bdbf3f2359..22db871ccd 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -31,11 +31,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/plugin-kubernetes-common": "^0.1.5", "@backstage/theme": "^0.2.11", "@kubernetes/client-node": "^0.15.0", @@ -51,8 +51,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index a6b681d523..8d9cf70bad 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -32,11 +32,11 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index ac49033a1b..c44608e2ef 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,8 +43,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/org/package.json b/plugins/org/package.json index 2edc9b5ffe..4e89865f44 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,8 +37,8 @@ "qs": "^6.10.1" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index b88212a743..35a240b5de 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index e7510450b0..c43ea8b31a 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/config": "^0.1.8", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -43,7 +43,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 7c872cc704..1d7be09b85 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.4", + "@backstage/backend-common": "^0.9.7", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.13", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index ea641f7f78..f35852fe74 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,8 +49,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 88eef336a3..7db57dbe6b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/errors": "^0.1.1", + "@backstage/backend-common": "^0.9.7", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.2", - "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/plugin-scaffolder-backend": "^0.15.10", "@backstage/config": "^0.1.8", "command-exists": "^1.2.9", "fs-extra": "10.0.0", @@ -32,7 +32,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.8.0", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index f0cd953a5c..c88e6d0f9f 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -21,16 +21,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/backend-common": "^0.9.7", + "@backstage/plugin-scaffolder-backend": "^0.15.10", "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.2", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 160a0229d5..d027ec52e7 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend +## 0.15.10 + +### Patch Changes + +- b149e94290: Allow `catalog:register` action to register optional locations +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.1 + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.15.9 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 05377123c0..ea29866485 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.9", + "version": "0.15.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,13 +30,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-backend": "^0.17.0", + "@backstage/plugin-catalog-backend": "^0.17.1", "@backstage/plugin-scaffolder-common": "^0.1.0", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", "@gitbeaker/core": "^30.2.0", @@ -70,7 +70,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.16", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 7f79dab541..b0c8de30c7 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -36,10 +36,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10" }, "devDependencies": { - "@backstage/cli": "^0.7.15" + "@backstage/cli": "^0.8.0" } } diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 6f472e8ab1..10483f4807 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -32,14 +32,14 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -64,8 +64,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 35d4a47277..87688e455f 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -30,8 +30,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/cli": "^0.7.12", + "@backstage/backend-common": "^0.9.7", + "@backstage/cli": "^0.8.0", "@elastic/elasticsearch-mock": "^0.3.0" }, "files": [ diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index a7196bc283..a31d908ac4 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -20,15 +20,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.4", + "@backstage/backend-common": "^0.9.7", "@backstage/search-common": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.4.2", "lodash": "^4.17.21", "knex": "^0.95.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.6", - "@backstage/cli": "^0.7.13" + "@backstage/backend-test-utils": "^0.1.8", + "@backstage/cli": "^0.8.0" }, "files": [ "dist", diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index b2f835d16d..6681d31819 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -26,8 +26,8 @@ "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.9.1", - "@backstage/cli": "^0.7.10" + "@backstage/backend-common": "^0.9.7", + "@backstage/cli": "^0.8.0" }, "files": [ "dist" diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 7d1f4d1cab..18d4484451 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.1", + "@backstage/backend-common": "^0.9.7", "@backstage/search-common": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.4.2", "@types/express": "^4.17.6", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.10", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search/package.json b/plugins/search/package.json index 7f6858f42b..9830d26e5c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -50,8 +50,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 3ed90a617f..cfaab7754c 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sentry +## 0.3.26 + +### Patch Changes + +- f93e4349e5: fix: parameter name on call to sentry api +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.3.25 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 6703332aae..ae3aad815f 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.25", + "version": "0.3.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index b4bfcd9a95..c9afe49c5a 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,8 +37,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 6dc132a366..971eb1e8cb 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -33,10 +33,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,8 +49,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 4d853be4dd..007707a735 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 79d5fc1487..bf146c4c2a 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -31,8 +31,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,8 +45,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 7b88ae0c41..221a681203 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs-backend +## 0.10.5 + +### Patch Changes + +- 177401b571: Display entity title (if defined) in titles of TechDocs search results +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/techdocs-common@0.10.4 + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + ## 0.10.4 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index cac7dcf422..c4fec93c10 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.10.4", + "version": "0.10.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.7", "@backstage/search-common": "^0.2.0", - "@backstage/techdocs-common": "^0.10.3", + "@backstage/techdocs-common": "^0.10.4", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", @@ -51,7 +51,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.18", "@types/dockerode": "^3.2.1", "msw": "^0.29.0", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 1fbdc22195..f3b94c8579 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-techdocs +## 0.12.3 + +### Patch Changes + +- ba5b75ed2f: Add `` as an alternative to `` that + shows a grid of card instead of table. + + Extend `` to display the entity title of the entity instead of the + name if available. + +- 177401b571: Display entity title (if defined) in titles of TechDocs search results +- cdf8ca6111: Only replace the shadow dom if the content is changed to avoid a flickering UI. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog@0.7.2 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + ## 0.12.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 762c52efd9..0aecd7d17c 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.12.2", + "version": "0.12.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,15 +32,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/plugin-catalog": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/plugin-search": "^0.4.15", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", @@ -61,8 +61,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 2e945487a4..67c990cf13 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -25,11 +25,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.7", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", @@ -40,7 +40,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 1c1af5e360..d20ff43470 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -27,11 +27,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.2", - "@backstage/plugin-catalog-react": "^0.6.0", + "@backstage/catalog-model": "^0.9.5", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -41,8 +41,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 0cc6dbd72e..ae3bc1c72d 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-user-settings +## 0.3.10 + +### Patch Changes + +- 63602a1753: Align grid height +- 202f322927: Atlassian auth provider + + - AtlassianAuth added to core-app-api + - Atlassian provider added to plugin-auth-backend + - Updated user-settings with Atlassian connection + +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + ## 0.3.9 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 44f33df9c8..bc836a7251 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.3.9", + "version": "0.3.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,9 +43,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", - "@backstage/core-plugin-api": "^0.1.10", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", + "@backstage/core-plugin-api": "^0.1.11", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 2d2f9983d6..c012c8e059 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.0", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/errors": "^0.1.1", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,8 +36,8 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.16", - "@backstage/core-app-api": "^0.1.17", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", From 109a66a7c7d549cedc128e4e73541e9dad185952 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 21 Oct 2021 09:42:52 -0400 Subject: [PATCH 055/245] make bucketRootPath the last argument to publish constructors Signed-off-by: Colton Padden --- packages/techdocs-common/src/stages/publish/awsS3.ts | 6 +++--- .../techdocs-common/src/stages/publish/googleStorage.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 348a17e35a..317f296fce 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -114,9 +114,9 @@ export class AwsS3Publish implements PublisherBase { return new AwsS3Publish( storageClient, bucketName, - bucketRootPath, legacyPathCasing, logger, + bucketRootPath, ); } @@ -154,15 +154,15 @@ export class AwsS3Publish implements PublisherBase { constructor( private readonly storageClient: aws.S3, private readonly bucketName: string, - private readonly bucketRootPath: string, private readonly legacyPathCasing: boolean, private readonly logger: Logger, + private readonly bucketRootPath: string, ) { this.storageClient = storageClient; this.bucketName = bucketName; - this.bucketRootPath = bucketRootPath; this.legacyPathCasing = legacyPathCasing; this.logger = logger; + this.bucketRootPath = bucketRootPath; } /** diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index e1c0a2b7ea..51be9eeea5 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -86,24 +86,24 @@ export class GoogleGCSPublish implements PublisherBase { return new GoogleGCSPublish( storageClient, bucketName, - bucketRootPath, legacyPathCasing, logger, + bucketRootPath, ); } constructor( private readonly storageClient: Storage, private readonly bucketName: string, - private readonly bucketRootPath: string, private readonly legacyPathCasing: boolean, private readonly logger: Logger, + private readonly bucketRootPath: string, ) { this.storageClient = storageClient; this.bucketName = bucketName; - this.bucketRootPath = bucketRootPath; this.legacyPathCasing = legacyPathCasing; this.logger = logger; + this.bucketRootPath = bucketRootPath; } /** From 7c5cf5a4a443dd20f26bf2142ac4f5cc15195689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 Oct 2021 16:07:35 +0200 Subject: [PATCH 056/245] add test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/types/package.json | 4 +- packages/types/src/json.test.ts | 81 +++++++++++++++++++++++++++ packages/types/src/observable.test.ts | 42 ++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 packages/types/src/json.test.ts create mode 100644 packages/types/src/observable.test.ts diff --git a/packages/types/package.json b/packages/types/package.json index 8274c1ce74..e8117fff37 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -31,7 +31,9 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.7.11" + "@backstage/cli": "^0.7.11", + "@types/zen-observable": "^0.8.0", + "zen-observable": "^0.8.15" }, "files": [ "dist" diff --git a/packages/types/src/json.test.ts b/packages/types/src/json.test.ts new file mode 100644 index 0000000000..7da0ef8a3e --- /dev/null +++ b/packages/types/src/json.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { JsonPrimitive, JsonArray, JsonObject, JsonValue } from './json'; + +describe('json', () => { + it('JsonPrimitive', () => { + function isValid(..._v: JsonPrimitive[]) {} + isValid(1, 's', true, false, null); + + // @ts-expect-error + const v1: JsonPrimitive = []; + // @ts-expect-error + const v2: JsonPrimitive = {}; + + expect(true).toBe(true); + }); + + it('JsonArray', () => { + function isValid(..._v: JsonArray[]) {} + isValid([], [1, 's', true, false, null, {}, []]); + + // @ts-expect-error + const v1: JsonArray = 1; + // @ts-expect-error + const v2: JsonArray = 's'; + // @ts-expect-error + const v3: JsonArray = true; + // @ts-expect-error + const v4: JsonArray = false; + // @ts-expect-error + const v5: JsonArray = null; + // @ts-expect-error + const v6: JsonArray = {}; + + expect(true).toBe(true); + }); + + it('JsonObject', () => { + function isValid(..._v: JsonObject[]) {} + isValid( + {}, + { v1: 1, v2: 's', v3: true, v4: false, v5: null, v6: {}, v7: [] }, + ); + + // @ts-expect-error + const v1: JsonObject = 1; + // @ts-expect-error + const v2: JsonObject = 's'; + // @ts-expect-error + const v3: JsonObject = true; + // @ts-expect-error + const v4: JsonObject = false; + // @ts-expect-error + const v5: JsonObject = null; + // @ts-expect-error + const v6: JsonObject = []; + + expect(true).toBe(true); + }); + + it('JsonValue', () => { + function isValid(..._v: JsonValue[]) {} + isValid(1, 's', true, false, null, {}, []); + + expect(true).toBe(true); + }); +}); diff --git a/packages/types/src/observable.test.ts b/packages/types/src/observable.test.ts new file mode 100644 index 0000000000..77cd074fa3 --- /dev/null +++ b/packages/types/src/observable.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ZenObservable from 'zen-observable'; +import { Observable, Observer, Subscription } from './observable'; + +describe('observable', () => { + it('works in conjunction with zen-observables', () => { + // Use ZenObservable as the concrete implementation, but use our types in + // all other possible places in the code + const observable: Observable = new ZenObservable( + (subscriber: Observer) => { + subscriber.next!('a'); + subscriber.error!(new Error('e')); + subscriber.complete!(); + }, + ); + + const subscription: Subscription = observable.subscribe( + _value => {}, + _error => {}, + () => {}, + ); + + subscription.unsubscribe(); + + expect(true).toBe(true); + }); +}); From 66bcc31ec42d91d40b5a46ccbe1652ee7056b208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 Oct 2021 16:36:41 +0200 Subject: [PATCH 057/245] some more fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/core-plugin-api/api-report.md | 6 ------ packages/core-plugin-api/src/deprecatedTypes.ts | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 0347b4b018..575d0b755f 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -683,13 +683,9 @@ export const oauthRequestApiRef: ApiRef; // @public export type OAuthScope = string | string[]; -// Warning: (ae-missing-release-tag) "Observable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export type Observable = Observable_2; -// Warning: (ae-missing-release-tag) "Observer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export type Observer = Observer_2; @@ -897,8 +893,6 @@ export type SubRouteRef = { params: ParamKeys; }; -// Warning: (ae-missing-release-tag) "Subscription" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export type Subscription = Subscription_2; diff --git a/packages/core-plugin-api/src/deprecatedTypes.ts b/packages/core-plugin-api/src/deprecatedTypes.ts index 605dc72ee3..4b24614e17 100644 --- a/packages/core-plugin-api/src/deprecatedTypes.ts +++ b/packages/core-plugin-api/src/deprecatedTypes.ts @@ -23,6 +23,7 @@ import { /** * Observer interface for consuming an Observer, see TC39. * + * @public * @deprecated Please use the same type from `@backstage/types` instead */ export type Observer = CoreObserver; @@ -30,6 +31,7 @@ export type Observer = CoreObserver; /** * Subscription returned when subscribing to an Observable, see TC39. * + * @public * @deprecated Please use the same type from `@backstage/types` instead */ export type Subscription = CoreSubscription; @@ -42,6 +44,7 @@ export type Subscription = CoreSubscription; * This is used as a common return type for observable values and can be created * using many different observable implementations, such as zen-observable or RxJS 5. * + * @public * @deprecated Please use the same type from `@backstage/types` instead */ export type Observable = CoreObservable; From a9a8c6f7c51793fbc4727ca409ae52d89ea7e8ca Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 21 Oct 2021 10:36:59 -0400 Subject: [PATCH 058/245] Scroll to the top of reader content when primary navigation link is clicked When navigating between pages using the primary sidebar navigation menu, scroll to the top of the generated markdown content via `.scrollIntoView()`. Signed-off-by: Colton Padden --- .changeset/dull-news-visit.md | 5 +++++ plugins/techdocs/src/reader/components/Reader.tsx | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/dull-news-visit.md diff --git a/.changeset/dull-news-visit.md b/.changeset/dull-news-visit.md new file mode 100644 index 0000000000..e1fab41ebf --- /dev/null +++ b/.changeset/dull-news-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Reader will now scroll to the top of the page when navigating between pages diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 7307961772..9bb0a7bbad 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -321,6 +321,7 @@ export const useTechDocsReaderDom = (): Element | null => { baseUrl: window.location.origin, onClick: (_: MouseEvent, url: string) => { const parsedUrl = new URL(url); + // hash exists when anchor is clicked on secondary sidebar if (parsedUrl.hash) { navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); // Scroll to hash if it's on the current page @@ -329,6 +330,10 @@ export const useTechDocsReaderDom = (): Element | null => { ?.scrollIntoView(); } else { navigate(parsedUrl.pathname); + // Scroll to top of reader if primary sidebar link is clicked + transformedElement + ?.querySelector('.md-content__inner') + ?.scrollIntoView(); } }, }), From 8c30ae89025d054c8ec1afc8ea80eee11ee4e6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 Oct 2021 21:16:35 +0200 Subject: [PATCH 059/245] Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/wet-socks-grow.md | 5 ++++ packages/errors/api-report.md | 5 +++- packages/errors/src/errors/CustomErrorBase.ts | 20 +++---------- .../errors/src/serialization/error.test.ts | 29 ++++++++++++++++++- packages/errors/src/serialization/error.ts | 17 +++++++++++ packages/errors/src/serialization/index.ts | 2 +- .../catalog-backend/src/stitching/Stitcher.ts | 6 ++-- 7 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 .changeset/wet-socks-grow.md diff --git a/.changeset/wet-socks-grow.md b/.changeset/wet-socks-grow.md new file mode 100644 index 0000000000..8ccbb7a2a2 --- /dev/null +++ b/.changeset/wet-socks-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/errors': patch +--- + +Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index 6552e111ce..ffb111884e 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -18,7 +18,7 @@ export class ConflictError extends CustomErrorBase {} export class CustomErrorBase extends Error { constructor(message?: string, cause?: Error | unknown); // (undocumented) - readonly cause?: Error; + readonly cause?: Error | undefined; } // @public @@ -98,4 +98,7 @@ export function serializeError( includeStack?: boolean; }, ): SerializedError; + +// @public +export function stringifyError(error: unknown): string; ``` diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts index 5c5c032de2..17149cd398 100644 --- a/packages/errors/src/errors/CustomErrorBase.ts +++ b/packages/errors/src/errors/CustomErrorBase.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { stringifyError } from '../serialization'; import { isError } from './assertion'; /** @@ -32,25 +33,12 @@ import { isError } from './assertion'; * ``` */ export class CustomErrorBase extends Error { - readonly cause?: Error; + readonly cause?: Error | undefined; constructor(message?: string, cause?: Error | unknown) { - let assignedCause: Error | undefined = undefined; - let fullMessage = message; if (cause !== undefined) { - let causeStr; - if (isError(cause)) { - assignedCause = cause; - causeStr = String(cause); - - // Prefer the cause.toString, but if it's not implemented we use a nicer fallback - if (causeStr === '[object Object]') { - causeStr = `${cause.name}: ${cause.message}`; - } - } else { - causeStr = `unknown error '${cause}'`; - } + const causeStr = stringifyError(cause); if (fullMessage) { fullMessage += `; caused by ${causeStr}`; } else { @@ -63,6 +51,6 @@ export class CustomErrorBase extends Error { Error.captureStackTrace?.(this, this.constructor); this.name = this.constructor.name; - this.cause = assignedCause; + this.cause = isError(cause) ? cause : undefined; } } diff --git a/packages/errors/src/serialization/error.test.ts b/packages/errors/src/serialization/error.test.ts index 27691e03f2..a07d11749a 100644 --- a/packages/errors/src/serialization/error.test.ts +++ b/packages/errors/src/serialization/error.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { deserializeError, serializeError } from './error'; +import { NotModifiedError } from '../errors'; +import { deserializeError, serializeError, stringifyError } from './error'; class CustomError extends Error { readonly customField: any; @@ -61,4 +62,30 @@ describe('serialization', () => { expect(withoutStack1.stack).not.toBeDefined(); expect(withoutStack2.stack).not.toBeDefined(); }); + + it('stringifies all supported forms', () => { + expect(stringifyError({})).toEqual("unknown error '[object Object]'"); + expect( + stringifyError({ + toString() { + return 'str'; + }, + }), + ).toEqual("unknown error 'str'"); + expect( + stringifyError({ + name: 'not used', + message: 'not used', + toString() { + return 'str'; + }, + }), + ).toEqual('str'); + expect(stringifyError({ name: 'N', message: 'm1' })).toEqual('N: m1'); + expect(stringifyError(new NotModifiedError('m2'))).toEqual( + 'NotModifiedError: m2', + ); + expect(stringifyError(new Error('m3'))).toEqual('Error: m3'); + expect(stringifyError(new TypeError('m4'))).toEqual('TypeError: m4'); + }); }); diff --git a/packages/errors/src/serialization/error.ts b/packages/errors/src/serialization/error.ts index ab543164d8..b1f63063cf 100644 --- a/packages/errors/src/serialization/error.ts +++ b/packages/errors/src/serialization/error.ts @@ -19,6 +19,7 @@ import { deserializeError as deserializeErrorInternal, serializeError as serializeErrorInternal, } from 'serialize-error'; +import { isError } from '../errors'; /** * The serialized form of an Error. @@ -78,3 +79,19 @@ export function deserializeError( } return result; } + +/** + * Stringifies an error, including its name and message where available. + * + * @param error - The error. + * @public + */ +export function stringifyError(error: unknown): string { + if (isError(error)) { + // Prefer error.toString, but if it's not implemented we use a nicer fallback + const str = String(error); + return str !== '[object Object]' ? str : `${error.name}: ${error.message}`; + } + + return `unknown error '${error}'`; +} diff --git a/packages/errors/src/serialization/index.ts b/packages/errors/src/serialization/index.ts index a4ade6df95..d37b04297a 100644 --- a/packages/errors/src/serialization/index.ts +++ b/packages/errors/src/serialization/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { deserializeError, serializeError } from './error'; +export { deserializeError, serializeError, stringifyError } from './error'; export type { SerializedError } from './error'; export { parseErrorResponse } from './response'; export type { ErrorResponse } from './response'; diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index 1d40340b3e..507f3aa53e 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -20,7 +20,7 @@ import { parseEntityRef, UNSTABLE_EntityStatusItem, } from '@backstage/catalog-model'; -import { SerializedError } from '@backstage/errors'; +import { SerializedError, stringifyError } from '@backstage/errors'; import { Knex } from 'knex'; import { uniqBy } from 'lodash'; import { v4 as uuid } from 'uuid'; @@ -49,7 +49,9 @@ export class Stitcher { try { await this.stitchOne(entityRef); } catch (error) { - this.logger.error(`Failed to stitch ${entityRef}, ${error}`); + this.logger.error( + `Failed to stitch ${entityRef}, ${stringifyError(error)}`, + ); } } } From 5a58092168be995a31d9d49bb26b7e70caec068c Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 21 Oct 2021 16:32:45 +0100 Subject: [PATCH 060/245] no longer expose the storage task broker We can implement the task broker completely, so it is not neccessary to export it. This commit also addresses some review comments. Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 178 ++++++++---------- plugins/scaffolder-backend/src/index.ts | 7 + .../src/scaffolder/index.ts | 25 +-- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 17 +- .../scaffolder/tasks/DefaultWorkflowRunner.ts | 8 +- .../scaffolder/tasks/LegacyWorkflowRunner.ts | 4 - .../tasks/StorageTaskBroker.test.ts | 6 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 27 +-- .../src/scaffolder/tasks/TaskWorker.ts | 58 +++++- .../src/scaffolder/tasks/index.ts | 4 +- .../src/scaffolder/tasks/types.ts | 56 +++--- .../scaffolder-backend/src/service/router.ts | 9 +- 12 files changed, 215 insertions(+), 184 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index e0e7ad7ee5..f47c550875 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -26,7 +26,6 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { UrlReader } from '@backstage/backend-common'; -import * as winston from 'winston'; import { Writable } from 'stream'; // Warning: (ae-forgotten-export) The symbol "InputBase" needs to be exported by the entry point index.d.ts @@ -57,6 +56,9 @@ export class CatalogEntityClient { ): Promise; } +// @public +export type CompletedTaskState = 'failed' | 'completed'; + // Warning: (ae-missing-release-tag) "createBuiltinActions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -191,11 +193,22 @@ export const createTemplateAction: < templateAction: TemplateAction, ) => TemplateAction; +// Warning: (ae-missing-release-tag) "CreateWorkerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type CreateWorkerOptions = { + taskBroker: TaskBroker; + actionRegistry: TemplateActionRegistry; + integrations: ScmIntegrations; + workingDirectory: string; + logger: Logger_2; +}; + // @public export class DatabaseTaskStore implements TaskStore { constructor(db: Knex); // (undocumented) - claimTask(): Promise; + claimTask(): Promise; // (undocumented) completeTask({ taskId, @@ -218,14 +231,12 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; // (undocumented) - getTask(taskId: string): Promise; + getTask(taskId: string): Promise; // (undocumented) heartbeatTask(taskId: string): Promise; - // Warning: (ae-forgotten-export) The symbol "TaskStoreGetEventsOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - listEvents({ taskId, after }: TaskStoreGetEventsOptions): Promise<{ - events: DbTaskEventRow[]; + listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ + events: SerializedTaskEvent[]; }>; // (undocumented) listStaleTasks({ timeoutS }: { timeoutS: number }): Promise<{ @@ -235,29 +246,6 @@ export class DatabaseTaskStore implements TaskStore { }>; } -// @public -export type DbTaskRow = { - id: string; - spec: TaskSpec; - status: Status; - createdAt: string; - lastHeartbeatAt?: string; - secrets?: TaskSecrets; -}; - -// Warning: (ae-forgotten-export) The symbol "WorkflowRunner" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "DefaultWorkflowRunner" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class DefaultWorkflowRunner implements WorkflowRunner { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options_3); - // Warning: (ae-forgotten-export) The symbol "WorkflowResponse" needs to be exported by the entry point index.d.ts - // - // (undocumented) - execute(task: Task): Promise; -} - // @public export type DispatchResult = { taskId: string; @@ -280,14 +268,6 @@ export function fetchContents({ outputPath: string; }): Promise; -// @public -export class LegacyWorkflowRunner implements WorkflowRunner { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options_2); - // (undocumented) - execute(task: Task): Promise; -} - // Warning: (ae-missing-release-tag) "OctokitProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -299,17 +279,6 @@ export class OctokitProvider { } // @public -export type RawDbTaskEventRow = { - id: number; - task_id: string; - body: string; - event_type: TaskEventType; - created_at: string; -}; - -// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) export interface RouterOptions { // (undocumented) actions?: TemplateAction[]; @@ -353,6 +322,25 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { validateEntityKind(entity: Entity): Promise; } +// @public +export type SerializedTask = { + id: string; + spec: TaskSpec; + status: Status; + createdAt: string; + lastHeartbeatAt?: string; + secrets?: TaskSecrets; +}; + +// @public +export type SerializedTaskEvent = { + id: number; + taskId: string; + body: JsonObject; + type: TaskEventType; + createdAt: string; +}; + // @public export type Status = | 'open' @@ -361,40 +349,8 @@ export type Status = | 'cancelled' | 'completed'; -// @public -export class StorageTaskBroker implements TaskBroker { - constructor(storage: TaskStore, logger: Logger_2); - // (undocumented) - claim(): Promise; - // (undocumented) - dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; - // (undocumented) - get(taskId: string): Promise; - // (undocumented) - protected readonly logger: Logger_2; - // (undocumented) - observe( - options: { - taskId: string; - after: number | undefined; - }, - callback: ( - error: Error | undefined, - result: { - events: DbTaskEventRow[]; - }, - ) => void, - ): () => void; - // (undocumented) - protected readonly storage: TaskStore; - // (undocumented) - vacuumTasks(timeoutS: { timeoutS: number }): Promise; -} - // @public export interface Task { - // Warning: (ae-forgotten-export) The symbol "CompletedTaskState" needs to be exported by the entry point index.d.ts - // // (undocumented) complete(result: CompletedTaskState, metadata?: JsonValue): Promise; // (undocumented) @@ -409,14 +365,10 @@ export interface Task { spec: TaskSpec; } -// Warning: (ae-missing-release-tag) "TaskAgent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class TaskAgent implements Task { // (undocumented) complete(result: CompletedTaskState, metadata?: JsonObject): Promise; - // Warning: (ae-forgotten-export) The symbol "TaskState" needs to be exported by the entry point index.d.ts - // // (undocumented) static create( state: TaskState, @@ -440,12 +392,9 @@ export interface TaskBroker { // (undocumented) claim(): Promise; // (undocumented) - dispatch( - spec: TaskSpec, - secretTaskSecretss?: TaskSecrets, - ): Promise; + dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; // (undocumented) - get(taskId: string): Promise; + get(taskId: string): Promise; // (undocumented) observe( options: { @@ -455,7 +404,7 @@ export interface TaskBroker { callback: ( error: Error | undefined, result: { - events: DbTaskEventRow[]; + events: SerializedTaskEvent[]; }, ) => void, ): () => void; @@ -463,9 +412,6 @@ export interface TaskBroker { vacuumTasks(timeoutS: { timeoutS: number }): Promise; } -// @public -export type TaskEventType = 'completion' | 'log'; - // @public export type TaskSecrets = { token: string | undefined; @@ -514,10 +460,20 @@ export interface TaskSpecV1beta3 { steps: TaskStep[]; } +// @public +export interface TaskState { + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; + // (undocumented) + taskId: string; +} + // @public export interface TaskStore { // (undocumented) - claimTask(): Promise; + claimTask(): Promise; // (undocumented) completeTask(options: { taskId: string; @@ -534,12 +490,12 @@ export interface TaskStore { // (undocumented) emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; // (undocumented) - getTask(taskId: string): Promise; + getTask(taskId: string): Promise; // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - listEvents({ taskId, after }: TaskStoreGetEventsOptions): Promise<{ - events: DbTaskEventRow[]; + listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ + events: SerializedTaskEvent[]; }>; // (undocumented) listStaleTasks(options: { timeoutS: number }): Promise<{ @@ -555,16 +511,32 @@ export type TaskStoreEmitOptions = { body: JsonObject; }; +// @public +export type TaskStoreListEventsOptions = { + taskId: string; + after?: number | undefined; +}; + // @public export class TaskWorker { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options); + constructor(options: TaskWorkerOptions); + // (undocumented) + static createWorker(options: CreateWorkerOptions): TaskWorker; // (undocumented) runOneTask(task: Task): Promise; // (undocumented) start(): void; } +// @public +export type TaskWorkerOptions = { + taskBroker: TaskBroker; + runners: { + legacyWorkflowRunner: LegacyWorkflowRunner; + workflowRunner: WorkflowRunner; + }; +}; + // Warning: (ae-missing-release-tag) "TemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -594,5 +566,7 @@ export class TemplateActionRegistry { // Warnings were encountered during analysis: // -// src/scaffolder/tasks/DatabaseTaskStore.d.ts:51:9 - (ae-forgotten-export) The symbol "DbTaskEventRow" needs to be exported by the entry point index.d.ts +// src/scaffolder/tasks/TaskWorker.d.ts:14:9 - (ae-forgotten-export) The symbol "LegacyWorkflowRunner" needs to be exported by the entry point index.d.ts +// src/scaffolder/tasks/TaskWorker.d.ts:15:9 - (ae-forgotten-export) The symbol "WorkflowRunner" needs to be exported by the entry point index.d.ts +// src/scaffolder/tasks/types.d.ts:37:5 - (ae-forgotten-export) The symbol "TaskEventType" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index 76cda63eef..4c31e9f982 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -24,3 +24,10 @@ export * from './scaffolder'; export * from './service/router'; export * from './lib/catalog'; export * from './processor'; +export { TaskAgent } from './scaffolder/tasks'; +export type { + TaskBroker, + DispatchResult, + TaskStore, + Task, +} from './scaffolder/tasks/types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index cf2a554189..b2599f20ed 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -14,27 +14,18 @@ * limitations under the License. */ export * from './actions'; -export { - TaskAgent, - DatabaseTaskStore, - StorageTaskBroker, - TaskWorker, - LegacyWorkflowRunner, - DefaultWorkflowRunner, -} from './tasks'; +export { DatabaseTaskStore, TaskWorker } from './tasks'; +export type { TaskWorkerOptions, CreateWorkerOptions } from './tasks'; +export type { TaskState } from './tasks'; export type { - Task, - TaskBroker, - TaskSpec, TaskSecrets, - DispatchResult, - TaskStore, + TaskSpec, + CompletedTaskState, TaskStoreEmitOptions, - DbTaskRow, + TaskStoreListEventsOptions, + SerializedTask, + SerializedTaskEvent, TaskSpecV1beta2, TaskSpecV1beta3, Status, - TaskEventType, } from './tasks/types'; - -export type { RawDbTaskEventRow } from './tasks/DatabaseTaskStore'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 8f659cad02..3aec10d949 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -20,15 +20,15 @@ import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { - DbTaskEventRow, - DbTaskRow, + SerializedTaskEvent, + SerializedTask, Status, TaskEventType, TaskSecrets, TaskSpec, TaskStore, TaskStoreEmitOptions, - TaskStoreGetEventsOptions, + TaskStoreListEventsOptions, } from './types'; import { DateTime } from 'luxon'; @@ -46,10 +46,6 @@ export type RawDbTaskRow = { secrets?: string; }; -/** - * RawDbTaskEventRow - * @public - */ export type RawDbTaskEventRow = { id: number; task_id: string; @@ -60,6 +56,7 @@ export type RawDbTaskEventRow = { /** * DatabaseTaskStore + * * @public */ export class DatabaseTaskStore implements TaskStore { @@ -72,7 +69,7 @@ export class DatabaseTaskStore implements TaskStore { constructor(private readonly db: Knex) {} - async getTask(taskId: string): Promise { + async getTask(taskId: string): Promise { const [result] = await this.db('tasks') .where({ id: taskId }) .select(); @@ -109,7 +106,7 @@ export class DatabaseTaskStore implements TaskStore { return { taskId }; } - async claimTask(): Promise { + async claimTask(): Promise { return this.db.transaction(async tx => { const [task] = await tx('tasks') .where({ @@ -251,7 +248,7 @@ export class DatabaseTaskStore implements TaskStore { async listEvents({ taskId, after, - }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { + }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[] }> { const rawEvents = await this.db('task_events') .where({ task_id: taskId, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts index 7b2b437be2..145ded6bc0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts @@ -34,7 +34,7 @@ import { validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; -type Options = { +type NunjucksWorkflowRunnerOptions = { workingDirectory: string; actionRegistry: TemplateActionRegistry; integrations: ScmIntegrations; @@ -77,10 +77,6 @@ const createStepLogger = ({ task, step }: { task: Task; step: TaskStep }) => { return { taskLogger, streamLogger }; }; -/* - * DefaultWorkflowRunner - * @public - */ export class DefaultWorkflowRunner implements WorkflowRunner { private readonly nunjucks: nunjucks.Environment; @@ -92,7 +88,7 @@ export class DefaultWorkflowRunner implements WorkflowRunner { }, }; - constructor(private readonly options: Options) { + constructor(private readonly options: NunjucksWorkflowRunnerOptions) { this.nunjucks = nunjucks.configure(this.nunjucksOptions); // TODO(blam): let's work out how we can deprecate these. diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts index e71fe596cd..97e3a4dec4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts @@ -43,13 +43,9 @@ type Options = { const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => taskSpec.apiVersion === 'backstage.io/v1beta2'; - /** * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced * with the default workflow runner interface in the future so this entire thing can go bye bye. - * - * LegacyWorkflowRunner - * @public */ export class LegacyWorkflowRunner implements WorkflowRunner { private readonly handlebars: typeof Handlebars; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 1a4d0d68c5..183253ea0e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; -import { TaskSecrets, TaskSpec, DbTaskEventRow } from './types'; +import { TaskSecrets, TaskSpec, SerializedTaskEvent } from './types'; async function createStore(): Promise { const manager = DatabaseManager.fromConfig( @@ -127,8 +127,8 @@ describe('StorageTaskBroker', () => { const { taskId } = await broker1.dispatch({} as TaskSpec); - const logPromise = new Promise(resolve => { - const observedEvents = new Array(); + const logPromise = new Promise(resolve => { + const observedEvents = new Array(); broker2.observe({ taskId, after: undefined }, (_err, { events }) => { observedEvents.push(...events); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 59f605702d..af1f3752e1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -23,11 +23,13 @@ import { TaskStore, TaskBroker, DispatchResult, - DbTaskEventRow, - DbTaskRow, + SerializedTaskEvent, + SerializedTask, } from './types'; -/* +/** + * TaskAgent + * * @public */ export class TaskAgent implements Task { @@ -106,7 +108,12 @@ export class TaskAgent implements Task { } } -interface TaskState { +/** + * TaskState + * + * @public + */ +export interface TaskState { spec: TaskSpec; taskId: string; secrets?: TaskSecrets; @@ -120,14 +127,10 @@ function defer() { return { promise, resolve }; } -/** - * StorageTaskBroker - * @public - */ export class StorageTaskBroker implements TaskBroker { constructor( - protected readonly storage: TaskStore, - protected readonly logger: Logger, + private readonly storage: TaskStore, + private readonly logger: Logger, ) {} private deferredDispatch = defer(); @@ -161,7 +164,7 @@ export class StorageTaskBroker implements TaskBroker { }; } - async get(taskId: string): Promise { + async get(taskId: string): Promise { return this.storage.getTask(taskId); } @@ -172,7 +175,7 @@ export class StorageTaskBroker implements TaskBroker { }, callback: ( error: Error | undefined, - result: { events: DbTaskEventRow[] }, + result: { events: SerializedTaskEvent[] }, ) => void, ): () => void { const { taskId } = options; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 1f6d9d3381..65ba71e9fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -13,10 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Task, TaskBroker, WorkflowRunner } from './types'; import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; +import { DefaultWorkflowRunner } from './DefaultWorkflowRunner'; +import { Logger } from 'winston'; +import { TemplateActionRegistry } from '../actions'; +import { ScmIntegrations } from '@backstage/integration'; -type Options = { +/** + * TaskWorkerOptions + * + * @public + */ +export type TaskWorkerOptions = { taskBroker: TaskBroker; runners: { legacyWorkflowRunner: LegacyWorkflowRunner; @@ -24,12 +34,56 @@ type Options = { }; }; +/** + * CreateWorkerOptions + * + * @public + */ +export type CreateWorkerOptions = { + taskBroker: TaskBroker; + actionRegistry: TemplateActionRegistry; + integrations: ScmIntegrations; + workingDirectory: string; + logger: Logger; +}; + /** * TaskWorker + * * @public */ export class TaskWorker { - constructor(private readonly options: Options) {} + constructor(private readonly options: TaskWorkerOptions) {} + + static createWorker(options: CreateWorkerOptions) { + const { + taskBroker, + logger, + actionRegistry, + integrations, + workingDirectory, + } = options; + + const legacyWorkflowRunner = new LegacyWorkflowRunner({ + logger, + actionRegistry, + integrations, + workingDirectory, + }); + + const workflowRunner = new DefaultWorkflowRunner({ + actionRegistry, + integrations, + logger, + workingDirectory, + }); + + return new TaskWorker({ + taskBroker: taskBroker, + runners: { legacyWorkflowRunner, workflowRunner }, + }); + } + start() { (async () => { for (;;) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index c068e8d7e1..ada12df476 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -16,6 +16,6 @@ export { DatabaseTaskStore } from './DatabaseTaskStore'; export { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; +export type { TaskState } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; -export { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; -export { DefaultWorkflowRunner } from './DefaultWorkflowRunner'; +export type { TaskWorkerOptions, CreateWorkerOptions } from './TaskWorker'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 63b409e7ca..e30d156736 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -18,6 +18,7 @@ import { JsonValue, JsonObject } from '@backstage/config'; /** * Status + * * @public */ export type Status = @@ -27,13 +28,19 @@ export type Status = | 'cancelled' | 'completed'; +/** + * CompletedTaskState + * + * @public + */ export type CompletedTaskState = 'failed' | 'completed'; /** - * DbTaskRow + * SerializedTask + * * @public */ -export type DbTaskRow = { +export type SerializedTask = { id: string; spec: TaskSpec; status: Status; @@ -42,17 +49,14 @@ export type DbTaskRow = { secrets?: TaskSecrets; }; -/** - * TaskEventType - * @public - */ export type TaskEventType = 'completion' | 'log'; /** - * DbTaskEventRow + * SerializedTaskEvent + * * @public */ -export type DbTaskEventRow = { +export type SerializedTaskEvent = { id: number; taskId: string; body: JsonObject; @@ -62,6 +66,7 @@ export type DbTaskEventRow = { /** * TaskSpecV1beta2 + * * @public */ export interface TaskSpecV1beta2 { @@ -88,6 +93,7 @@ export interface TaskStep { /** * TaskSpecV1beta3 + * * @public */ export interface TaskSpecV1beta3 { @@ -100,12 +106,14 @@ export interface TaskSpecV1beta3 { /** * TaskSpec + * * @public */ export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; /** * TaskSecrets + * * @public */ export type TaskSecrets = { @@ -114,6 +122,7 @@ export type TaskSecrets = { /** * DispatchResult + * * @public */ export type DispatchResult = { @@ -122,6 +131,7 @@ export type DispatchResult = { /** * Task + * * @public */ export interface Task { @@ -135,14 +145,12 @@ export interface Task { /** * TaskBroker + * * @public */ export interface TaskBroker { claim(): Promise; - dispatch( - spec: TaskSpec, - secretTaskSecretss?: TaskSecrets, - ): Promise; + dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; vacuumTasks(timeoutS: { timeoutS: number }): Promise; observe( options: { @@ -151,15 +159,15 @@ export interface TaskBroker { }, callback: ( error: Error | undefined, - result: { events: DbTaskEventRow[] }, + result: { events: SerializedTaskEvent[] }, ) => void, ): () => void; - - get(taskId: string): Promise; + get(taskId: string): Promise; } /** * TaskStoreEmitOptions + * * @public */ export type TaskStoreEmitOptions = { @@ -167,13 +175,19 @@ export type TaskStoreEmitOptions = { body: JsonObject; }; -export type TaskStoreGetEventsOptions = { +/** + * TaskStoreListEventsOptions + * + * @public + */ +export type TaskStoreListEventsOptions = { taskId: string; after?: number | undefined; }; /** * TaskStore + * * @public */ export interface TaskStore { @@ -181,8 +195,8 @@ export interface TaskStore { task: TaskSpec, secrets?: TaskSecrets, ): Promise<{ taskId: string }>; - getTask(taskId: string): Promise; - claimTask(): Promise; + getTask(taskId: string): Promise; + claimTask(): Promise; completeTask(options: { taskId: string; status: Status; @@ -197,14 +211,10 @@ export interface TaskStore { listEvents({ taskId, after, - }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }>; + }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[] }>; } export type WorkflowResponse = { output: { [key: string]: JsonValue } }; -/* - * WorkflowRunner - * @public - */ export interface WorkflowRunner { execute(task: Task): Promise; } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index afc9cee986..d4d4dd0486 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -26,7 +26,6 @@ import { TaskWorker, } from '../scaffolder/tasks'; import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry'; -import { TaskBroker } from '../scaffolder'; import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; import { ContainerRunner, @@ -41,11 +40,15 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '../scaffolder/actions'; import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; - import { LegacyWorkflowRunner } from '../scaffolder/tasks/LegacyWorkflowRunner'; import { DefaultWorkflowRunner } from '../scaffolder/tasks/DefaultWorkflowRunner'; -import { TaskSpec } from '../scaffolder/tasks/types'; +import { TaskBroker, TaskSpec } from '../scaffolder/tasks/types'; +/** + * RouterOptions + * + * @public + */ export interface RouterOptions { logger: Logger; config: Config; From e55a5dea094b75318b6d5222924c29166900bb9f Mon Sep 17 00:00:00 2001 From: Kenneth Feng Date: Tue, 19 Oct 2021 21:49:41 -0400 Subject: [PATCH 061/245] plugin/scaffolder: set the file mode in publish:github:pull-request. The ability to set file mode was introduced in octokit-plugin-create-pull-request:v3.10. This PR updates the underlying octokit-plugin-create-pull-request version to 3.10, and sets the file mode when creating the pull request. Signed-off-by: Kenneth Feng --- .changeset/two-cougars-breathe.md | 5 ++ plugins/scaffolder-backend/package.json | 2 +- .../builtin/publish/githubPullRequest.test.ts | 86 ++++++++++++++++++- .../builtin/publish/githubPullRequest.ts | 19 +++- 4 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 .changeset/two-cougars-breathe.md diff --git a/.changeset/two-cougars-breathe.md b/.changeset/two-cougars-breathe.md new file mode 100644 index 0000000000..0b61078e85 --- /dev/null +++ b/.changeset/two-cougars-breathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed bug where the mode of an executable file was ignored diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ea29866485..643ab6e30c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -64,7 +64,7 @@ "luxon": "^2.0.2", "morgan": "^1.10.0", "nunjucks": "^3.2.3", - "octokit-plugin-create-pull-request": "^3.9.3", + "octokit-plugin-create-pull-request": "^3.10.0", "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0" diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index ed72b14a65..dbb5f3caff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -98,7 +98,11 @@ describe('createPublishGithubPullRequestAction', () => { { commit: 'Create my new app', files: { - 'file.txt': 'Hello there!', + 'file.txt': { + encoding: 'utf-8', + content: 'Hello there!', + mode: '100644', + }, }, }, ], @@ -167,7 +171,11 @@ describe('createPublishGithubPullRequestAction', () => { { commit: 'Create my new app', files: { - 'foo.txt': 'Hello there!', + 'foo.txt': { + content: 'Hello there!', + encoding: 'utf-8', + mode: '100644', + }, }, }, ], @@ -221,7 +229,79 @@ describe('createPublishGithubPullRequestAction', () => { { commit: 'Create my new app', files: { - 'file.txt': 'Hello there!', + 'file.txt': { + content: 'Hello there!', + encoding: 'utf-8', + mode: '100644', + }, + }, + }, + ], + }); + }); + + it('creates outputs for the url', async () => { + await instance.handler(ctx); + + expect(ctx.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + }); + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + }); + + describe('with executable file', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + description: 'This PR is really good', + }; + + mockFs({ + [workspacePath]: { + 'file.txt': mockFs.file({ + content: 'Hello there!', + mode: 33277, // File mode: 100755 + }), + }, + }); + + ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + }); + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + body: 'This PR is really good', + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: 'Hello there!', + encoding: 'utf-8', + mode: '100755', + }, }, }, ], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 253fd72b38..733fd673ff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { readFile } from 'fs-extra'; +import fs from 'fs-extra'; import path from 'path'; import { parseRepoUrl } from './util'; @@ -194,7 +194,20 @@ export const createPublishGithubPullRequestAction = ({ }); const fileContents = await Promise.all( - localFilePaths.map(p => readFile(path.resolve(fileRoot, p))), + localFilePaths.map(filePath => { + const absPath = path.resolve(fileRoot, filePath); + const content = fs.readFileSync(absPath).toString(); + const fileStat = fs.statSync(absPath); + const isExecutable = fileStat.mode === 33277 // aka. 100755; + // See the properties of tree items + // in https://docs.github.com/en/rest/reference/git#trees + const githubTreeItemMode = isExecutable ? '100755' : '100644'; + return { + encoding: 'utf-8', + content: content, + mode: githubTreeItemMode, + }; + }), ); const repoFilePaths = localFilePaths.map(repoFilePath => { @@ -205,7 +218,7 @@ export const createPublishGithubPullRequestAction = ({ { files: zipObject( repoFilePaths, - fileContents.map(buf => buf.toString()), + fileContents, ), commit: title, }, From 97231f55816098607dd5d68823bf830568839bee Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 21 Oct 2021 21:39:47 +0300 Subject: [PATCH 062/245] fix: sentry plugin allows search query parameter specified by documentation https://docs.sentry.io/api/events/list-a-projects-issues/ Signed-off-by: rodion --- .changeset/cold-wolves-eat.md | 5 +++++ plugins/sentry/api-report.md | 14 ++++++++++++-- plugins/sentry/src/api/production-api.ts | 10 ++++++++-- plugins/sentry/src/api/sentry-api.ts | 6 +++++- .../SentryIssuesWidget/SentryIssuesWidget.tsx | 6 ++++-- 5 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 .changeset/cold-wolves-eat.md diff --git a/.changeset/cold-wolves-eat.md b/.changeset/cold-wolves-eat.md new file mode 100644 index 0000000000..fa3a2180b2 --- /dev/null +++ b/.changeset/cold-wolves-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': patch +--- + +feature: sentry-plugin allows passing search query for listing issues diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index 7143272284..89c1dd1bca 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -36,7 +36,11 @@ export class MockSentryApi implements SentryApi { export class ProductionSentryApi implements SentryApi { constructor(discoveryApi: DiscoveryApi, organization: string); // (undocumented) - fetchIssues(project: string, statsFor: string): Promise; + fetchIssues( + project: string, + statsFor: string, + query: string, + ): Promise; } // Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -49,7 +53,11 @@ export const Router: ({ entity }: { entity: Entity }) => JSX.Element; // @public (undocumented) export interface SentryApi { // (undocumented) - fetchIssues(project: string, statsFor: string): Promise; + fetchIssues( + project: string, + statsFor: string, + query: string, + ): Promise; } // Warning: (ae-missing-release-tag) "sentryApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -100,10 +108,12 @@ export const SentryIssuesWidget: ({ entity, statsFor, variant, + query, }: { entity: Entity; statsFor?: '12h' | '24h' | undefined; variant?: InfoCardVariants | undefined; + query?: string | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "sentryPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/sentry/src/api/production-api.ts b/plugins/sentry/src/api/production-api.ts index e634650f96..d9ece065d3 100644 --- a/plugins/sentry/src/api/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -24,15 +24,21 @@ export class ProductionSentryApi implements SentryApi { private readonly organization: string, ) {} - async fetchIssues(project: string, statsFor: string): Promise { + async fetchIssues( + project: string, + statsFor: string, + query: string, + ): Promise { if (!project) { return []; } const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`; + const queryPart = query ? `&query=${query}` : ''; + const response = await fetch( - `${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}`, + `${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}${queryPart}`, ); if (response.status >= 400 && response.status < 600) { diff --git a/plugins/sentry/src/api/sentry-api.ts b/plugins/sentry/src/api/sentry-api.ts index a755ca0941..5c0fb318a6 100644 --- a/plugins/sentry/src/api/sentry-api.ts +++ b/plugins/sentry/src/api/sentry-api.ts @@ -23,5 +23,9 @@ export const sentryApiRef = createApiRef({ }); export interface SentryApi { - fetchIssues(project: string, statsFor: string): Promise; + fetchIssues( + project: string, + statsFor: string, + query: string, + ): Promise; } diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx index 46198e4b93..349ce61128 100644 --- a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -38,10 +38,12 @@ export const SentryIssuesWidget = ({ entity, statsFor = '24h', variant = 'gridItem', + query = '', }: { entity: Entity; statsFor?: '24h' | '12h'; variant?: InfoCardVariants; + query?: string; }) => { const errorApi = useApi(errorApiRef); const sentryApi = useApi(sentryApiRef); @@ -49,8 +51,8 @@ export const SentryIssuesWidget = ({ const projectId = useProjectSlug(entity); const { loading, value, error } = useAsync( - () => sentryApi.fetchIssues(projectId, statsFor), - [sentryApi, statsFor, projectId], + () => sentryApi.fetchIssues(projectId, statsFor, query), + [sentryApi, statsFor, projectId, query], ); useEffect(() => { From 97dc10706f9a8d9f2a42c562880f9f831d16631e Mon Sep 17 00:00:00 2001 From: Victor Perera Date: Thu, 21 Oct 2021 13:50:52 -0500 Subject: [PATCH 063/245] Changes requested fixed Signed-off-by: Victor Perera --- .../src/components/EntityTypePicker/EntityTypePicker.tsx | 8 +++----- .../src/components/EntityTypePicker/index.ts | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index 4f5cc96463..23451ebcfe 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,15 +22,13 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -export type EntityTypeFilterProps = { +type EntityTypeFilterProps = { initialFilter?: string; hidden?: boolean; }; -export const EntityTypePicker = ({ - hidden = false, - initialFilter, -}: EntityTypeFilterProps) => { +export const EntityTypePicker = (props: EntityTypeFilterProps) => { + const { hidden, initialFilter } = props; const alertApi = useApi(alertApiRef); const { error, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter(); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/index.ts b/plugins/catalog-react/src/components/EntityTypePicker/index.ts index 6e3fc60546..6e888516f3 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/index.ts +++ b/plugins/catalog-react/src/components/EntityTypePicker/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './EntityTypePicker'; +export { EntityTypePicker } from './EntityTypePicker'; From c671c3827939e6ffd51addac9650d98f540bf8d3 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 21 Oct 2021 14:53:29 -0400 Subject: [PATCH 064/245] Template directly to specified path otherwise use temp dir When the `--path` argument is specified, then perform the create-app templating directly to that path. Otherwise, template to a temporary directory, and move the files to a directory based on the project name. Removed the `cleanUp` method in favor of removing temporary files directly in the `moveApp` method in a `finally` block. Reverted `moveApp` method to use the move operation instead ofe recursively copying files. Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 70 +++++++++++----------------- 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 8f054319e0..d833f0c4d8 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -20,11 +20,10 @@ import chalk from 'chalk'; import { Command } from 'commander'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; -import { resolve as resolvePath, relative as relativePath } from 'path'; +import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; -import recursive from 'recursive-readdir'; const exec = promisify(execCb); @@ -69,12 +68,6 @@ async function createTemporaryAppFolder(tempDir: string) { }); } -async function cleanUp(tempDir: string) { - await Task.forItem('remove', 'temporary directory', async () => { - await fs.remove(tempDir); - }); -} - async function buildApp(appDir: string) { const runCmd = async (cmd: string) => { await Task.forItem('executing', cmd, async () => { @@ -94,28 +87,17 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - // Get the temporary files relative path in relation to the `tempDir` so - // that it can be used in resolving destination file path - const relativeTempFiles = await recursive(tempDir) - .then(files => { - return files.map(file => { - return relativePath(tempDir, file); - }); - }) + await fs + .move(tempDir, destination) .catch(error => { - throw new Error(`Failed to read temporary directory, ${error}`); - }); - - for (const relativeTempFile of relativeTempFiles) { - const tempFile = resolvePath(tempDir, relativeTempFile); - const destFile = resolvePath(destination, relativeTempFile); - - await fs.copy(tempFile, destFile).catch(error => { throw new Error( - `Failed to move file from ${tempFile} to ${destFile}, ${error}`, + `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, ); + }) + .finally(() => { + // remove temporary files on both success and failure + fs.removeSync(tempDir); }); - } }); } @@ -164,27 +146,30 @@ export default async (cmd: Command): Promise => { Task.log('Creating the app...'); try { - Task.section('Checking if the directory is available'); if (cmd.path) { + // Template directly to specified path + + Task.section('Checking that supplied path exists'); await checkPathExistsAndIsDirectory(appDir); + + Task.section('Preparing files'); + await templatingTask(templateDir, cmd.path, answers); } else { - // Directory must not already exist when using consructed path from - // `answers.name` + // Template to temporary location, and then move files + + Task.section('Checking if the directory is available'); await checkAppPathDoesntExist(paths.targetDir, answers.name); + + Task.section('Creating a temporary app directory'); + await createTemporaryAppFolder(tempDir); + + Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, answers); + + Task.section('Moving to final location'); + await moveApp(tempDir, appDir, answers.name); } - Task.section('Creating a temporary app directory'); - await createTemporaryAppFolder(tempDir); - - Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers); - - Task.section('Moving to final location'); - await moveApp(tempDir, appDir, answers.name); - - Task.section('Cleanup'); - await cleanUp(tempDir); - if (!cmd.skipInstall) { Task.section('Building the app'); await buildApp(appDir); @@ -207,10 +192,7 @@ export default async (cmd: Command): Promise => { Task.error(error.message); Task.log('It seems that something went wrong when creating the app 🤔'); - Task.log('We are going to clean up, and then you can try again.'); - Task.section('Cleanup'); - await cleanUp(tempDir); Task.error('🔥 Failed to create app!'); Task.exit(1); } From d8396253187c0062d3d3fe88edddbf47996f5eb2 Mon Sep 17 00:00:00 2001 From: Victor Perera Date: Thu, 21 Oct 2021 14:21:03 -0500 Subject: [PATCH 065/245] Update api-report Signed-off-by: Victor Perera --- plugins/catalog-react/api-report.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 705ce3feab..e255ea9974 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -645,14 +645,7 @@ export class EntityTypeFilter implements EntityFilter { readonly value: string | string[]; } -// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type EntityTypeFilterProps = { - initialFilter?: string; - hidden?: boolean; -}; - +// Warning: (ae-forgotten-export) The symbol "EntityTypeFilterProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From 9a9c630ad87e042b77b55616d0fedcceb61d60f8 Mon Sep 17 00:00:00 2001 From: mclarke47 Date: Thu, 21 Oct 2021 22:01:27 +0100 Subject: [PATCH 066/245] better changeset message Signed-off-by: mclarke47 --- .changeset/bright-plants-travel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bright-plants-travel.md b/.changeset/bright-plants-travel.md index cbfd73e648..ad8a77d17b 100644 --- a/.changeset/bright-plants-travel.md +++ b/.changeset/bright-plants-travel.md @@ -2,4 +2,4 @@ '@backstage/plugin-kubernetes': patch --- -Refresh Kubernetes plugin on interval +The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser. From 4f14d881c4972757e36a241317f06a0bc7cce984 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 13:56:45 +0200 Subject: [PATCH 067/245] Move test-utils-core into test-utils - Deprecates methods in test-utils-core - Migrates code intotest-utils - Updates documentation - Export missing types Signed-off-by: Johan Haals --- packages/test-utils-core/api-report.md | 41 +--- .../test-utils-core/src/testUtils/Keyboard.js | 3 + .../src/testUtils/logCollector.ts | 41 +++- .../src/testUtils/testingLibrary.ts | 16 +- packages/test-utils/api-report.md | 138 +++++++++-- packages/test-utils/package.json | 1 - packages/test-utils/src/index.ts | 1 - packages/test-utils/src/testUtils/Keyboard.js | 225 ++++++++++++++++++ .../test-utils/src/testUtils/Keyboard.test.js | 108 +++++++++ .../apis/AnalyticsApi/MockAnalyticsApi.ts | 5 + .../testUtils/apis/ErrorApi/MockErrorApi.ts | 20 +- .../src/testUtils/apis/ErrorApi/index.ts | 1 + .../apis/StorageApi/MockStorageApi.ts | 8 + .../test-utils/src/testUtils/appWrappers.tsx | 15 +- packages/test-utils/src/testUtils/index.tsx | 4 + .../src/testUtils/logCollector.test.ts | 96 ++++++++ .../test-utils/src/testUtils/logCollector.ts | 140 +++++++++++ .../src/testUtils/mockBreakpoint.ts | 2 +- .../src/testUtils/testingLibrary.ts | 42 ++++ 19 files changed, 831 insertions(+), 76 deletions(-) create mode 100644 packages/test-utils/src/testUtils/Keyboard.js create mode 100644 packages/test-utils/src/testUtils/Keyboard.test.js create mode 100644 packages/test-utils/src/testUtils/logCollector.test.ts create mode 100644 packages/test-utils/src/testUtils/logCollector.ts create mode 100644 packages/test-utils/src/testUtils/testingLibrary.ts diff --git a/packages/test-utils-core/api-report.md b/packages/test-utils-core/api-report.md index 4dad7a3a8a..60d4b5b0f7 100644 --- a/packages/test-utils-core/api-report.md +++ b/packages/test-utils-core/api-report.md @@ -6,21 +6,17 @@ import { ReactElement } from 'react'; import { RenderResult } from '@testing-library/react'; -// Warning: (ae-missing-release-tag) "AsyncLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type AsyncLogCollector = () => Promise; -// Warning: (ae-missing-release-tag) "CollectedLogs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type CollectedLogs = { [key in T]: string[]; }; -// Warning: (ae-missing-release-tag) "Keyboard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative // -// @public (undocumented) +// @public @deprecated (undocumented) export class Keyboard { constructor( target: any, @@ -66,48 +62,35 @@ export class Keyboard { static typeDebug(target: any, input: any): Promise; } -// Warning: (ae-missing-release-tag) "LogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type LogCollector = AsyncLogCollector | SyncLogCollector; -// Warning: (ae-missing-release-tag) "LogFuncs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type LogFuncs = 'log' | 'warn' | 'error'; -// Warning: (ae-missing-release-tag) "renderWithEffects" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated export function renderWithEffects(nodes: ReactElement): Promise; -// Warning: (ae-missing-release-tag) "SyncLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type SyncLogCollector = () => void; -// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "withLogCollector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated export function withLogCollector( callback: AsyncLogCollector, ): Promise>; -// @public (undocumented) +// @public @deprecated export function withLogCollector( callback: SyncLogCollector, ): CollectedLogs; -// @public (undocumented) +// @public @deprecated export function withLogCollector( logsToCollect: T[], callback: AsyncLogCollector, ): Promise>; -// @public (undocumented) +// @public @deprecated export function withLogCollector( logsToCollect: T[], callback: SyncLogCollector, diff --git a/packages/test-utils-core/src/testUtils/Keyboard.js b/packages/test-utils-core/src/testUtils/Keyboard.js index 3f45ca90f6..b0600eb78d 100644 --- a/packages/test-utils-core/src/testUtils/Keyboard.js +++ b/packages/test-utils-core/src/testUtils/Keyboard.js @@ -23,6 +23,9 @@ const codes = { Esc: 27, }; +/** + * @public + * @deprecated because of no usages. */ export class Keyboard { static async type(target, input) { await new Keyboard(target).type(input); diff --git a/packages/test-utils-core/src/testUtils/logCollector.ts b/packages/test-utils-core/src/testUtils/logCollector.ts index 3180010529..22f06e172b 100644 --- a/packages/test-utils-core/src/testUtils/logCollector.ts +++ b/packages/test-utils-core/src/testUtils/logCollector.ts @@ -16,36 +16,69 @@ /* eslint-disable no-console */ +/** + * @public + * @deprecated import from test-utils instead */ export type LogFuncs = 'log' | 'warn' | 'error'; +/** + * @public + * @deprecated import from test-utils instead */ export type AsyncLogCollector = () => Promise; +/** + * @public + * @deprecated import from test-utils instead */ export type SyncLogCollector = () => void; +/** + * @public + * @deprecated import from test-utils instead */ export type LogCollector = AsyncLogCollector | SyncLogCollector; +/** + * @public + * @deprecated import from test-utils instead */ export type CollectedLogs = { [key in T]: string[] }; const allCategories = ['log', 'warn', 'error']; -// Asynchronous log collector with that collects all categories +/** + * Asynchronous log collector with that collects all categories + * @public + * @deprecated import from test-utils instead */ export function withLogCollector( callback: AsyncLogCollector, ): Promise>; -// Synchronous log collector with that collects all categories +/** + * Synchronous log collector with that collects all categories + * @public + * @deprecated import from test-utils instead */ export function withLogCollector( callback: SyncLogCollector, ): CollectedLogs; -// Asynchronous log collector with that only collects selected categories +/** + * Asynchronous log collector with that only collects selected categories + * @public + * @deprecated import from test-utils instead + */ + export function withLogCollector( logsToCollect: T[], callback: AsyncLogCollector, ): Promise>; -// Synchronous log collector with that only collects selected categories +/** + * Synchronous log collector with that only collects selected categories + * @public + * @deprecated import from test-utils instead */ export function withLogCollector( logsToCollect: T[], callback: SyncLogCollector, ): CollectedLogs; +/** + * @public + * @deprecated import from test-utils instead + * */ export function withLogCollector( logsToCollect: LogFuncs[] | LogCollector, callback?: LogCollector, diff --git a/packages/test-utils-core/src/testUtils/testingLibrary.ts b/packages/test-utils-core/src/testUtils/testingLibrary.ts index 48e5132825..953a414968 100644 --- a/packages/test-utils-core/src/testUtils/testingLibrary.ts +++ b/packages/test-utils-core/src/testUtils/testingLibrary.ts @@ -18,11 +18,17 @@ import { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { render, RenderResult } from '@testing-library/react'; -// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async -// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit -// cleaner, since act doesn't return the result of the evaluated function. -// https://github.com/testing-library/react-testing-library/issues/281 -// https://github.com/facebook/react/pull/14853 +/** + * Simplifies rendering of async components in by taking care of the wrapping inside act + * @remarks + * Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async + * act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit + * cleaner, since act doesn't return the result of the evaluated function. + * https://github.com/testing-library/react-testing-library/issues/281 + * https://github.com/facebook/react/pull/14853 + * @public + * @deprecated import from test-utils instead + */ export async function renderWithEffects( nodes: ReactElement, ): Promise { diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index f4b3fe12bb..b432141fa1 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -17,9 +17,73 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueChange } from '@backstage/core-plugin-api'; -// Warning: (ae-missing-release-tag) "MockAnalyticsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type AsyncLogCollector = () => Promise; + +// @public +export type CollectedLogs = { + [key in T]: string[]; +}; + +// @public +export type ErrorWithContext = { + error: Error; + context?: ErrorContext; +}; + +// @public @deprecated (undocumented) +export class Keyboard { + constructor( + target: any, + { + debug, + }?: { + debug?: boolean | undefined; + }, + ); + // (undocumented) + click(): Promise; + // (undocumented) + debug: boolean; + // (undocumented) + document: any; + // (undocumented) + enter(value: any): Promise; + // (undocumented) + escape(): Promise; + // (undocumented) + get focused(): any; + // (undocumented) + static fromReadableInput(input: any): any; + // (undocumented) + _log(message: any, ...args: any[]): void; + // (undocumented) + _pretty(element: any): string; + // (undocumented) + send(chars: any): Promise; + // (undocumented) + _sendKey(key: any, charCode: any, action: any): Promise; + // (undocumented) + tab(): Promise; + // (undocumented) + static toReadableInput(chars: any): any; + // (undocumented) + toString(): string; + // (undocumented) + static type(target: any, input: any): Promise; + // (undocumented) + type(input: any): Promise; + // (undocumented) + static typeDebug(target: any, input: any): Promise; +} + +// @public +export type LogCollector = AsyncLogCollector | SyncLogCollector; + +// @public +export type LogFuncs = 'log' | 'warn' | 'error'; + +// @public export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) captureEvent({ @@ -33,9 +97,6 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "mockBreakpoint" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function mockBreakpoint({ matches, @@ -43,19 +104,14 @@ export function mockBreakpoint({ matches?: boolean | undefined; }): void; -// Warning: (ae-missing-release-tag) "MockErrorApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class MockErrorApi implements ErrorApi { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options?: Options); + constructor(options?: MockErrorApiOptions); // (undocumented) error$(): Observable<{ error: Error; context?: ErrorContext; }>; - // Warning: (ae-forgotten-export) The symbol "ErrorWithContext" needs to be exported by the entry point index.d.ts - // // (undocumented) getErrors(): ErrorWithContext[]; // (undocumented) @@ -64,9 +120,12 @@ export class MockErrorApi implements ErrorApi { waitForError(pattern: RegExp, timeoutMs?: number): Promise; } -// Warning: (ae-missing-release-tag) "MockStorageApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type MockErrorApiOptions = { + collect?: boolean; +}; + +// @public export class MockStorageApi implements StorageApi { // (undocumented) static create(data?: MockStorageBucket): MockStorageApi; @@ -82,9 +141,7 @@ export class MockStorageApi implements StorageApi { set(key: string, data: T): Promise; } -// Warning: (ae-missing-release-tag) "MockStorageBucket" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type MockStorageBucket = { [key: string]: any; }; @@ -100,22 +157,51 @@ export const msw: { }) => void; }; -// Warning: (ae-forgotten-export) The symbol "TestAppOptions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "renderInTestApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function renderInTestApp( Component: ComponentType | ReactNode, options?: TestAppOptions, ): Promise; -// Warning: (ae-missing-release-tag) "wrapInTestApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export function renderWithEffects(nodes: ReactElement): Promise; + +// @public +export type SyncLogCollector = () => void; + +// @public +export type TestAppOptions = { + routeEntries?: string[]; + mountedRoutes?: { + [path: string]: RouteRef | ExternalRouteRef; + }; +}; + +// @public +export function withLogCollector( + callback: AsyncLogCollector, +): Promise>; + +// @public +export function withLogCollector( + callback: SyncLogCollector, +): CollectedLogs; + +// @public +export function withLogCollector( + logsToCollect: T[], + callback: AsyncLogCollector, +): Promise>; + +// @public +export function withLogCollector( + logsToCollect: T[], + callback: SyncLogCollector, +): CollectedLogs; + // @public export function wrapInTestApp( Component: ComponentType | ReactNode, options?: TestAppOptions, ): ReactElement; - -export * from '@backstage/test-utils-core'; ``` diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 22dc950bd4..44a2bb1e15 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -31,7 +31,6 @@ "dependencies": { "@backstage/core-app-api": "^0.1.18", "@backstage/core-plugin-api": "^0.1.11", - "@backstage/test-utils-core": "^0.1.3", "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 545a5b9961..83a955baa7 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -21,4 +21,3 @@ */ export * from './testUtils'; -export * from '@backstage/test-utils-core'; diff --git a/packages/test-utils/src/testUtils/Keyboard.js b/packages/test-utils/src/testUtils/Keyboard.js new file mode 100644 index 0000000000..3f4724d562 --- /dev/null +++ b/packages/test-utils/src/testUtils/Keyboard.js @@ -0,0 +1,225 @@ +/* + * 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 { act, fireEvent } from '@testing-library/react'; + +const codes = { + Tab: 9, + Enter: 10, + Click: 17 /* This keyboard can click, deal with it */, + Esc: 27, +}; + +/** + * @public + * @deprecated because it has no usages. Perhaps resurfaced in the future when need be. + */ +export class Keyboard { + static async type(target, input) { + await new Keyboard(target).type(input); + } + + static async typeDebug(target, input) { + await new Keyboard(target, { debug: true }).type(input); + } + + static toReadableInput(chars) { + return chars.split('').map(char => { + switch (char.charCodeAt(0)) { + case codes.Tab: + return ''; + case codes.Enter: + return ''; + case codes.Click: + return ''; + case codes.Esc: + return ''; + default: + return char; + } + }); + } + + static fromReadableInput(input) { + return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => { + if (name in codes) { + return String.fromCharCode(codes[name]); + } + throw new Error(`Unknown char name: '${name}'`); + }); + } + + constructor(target, { debug = false } = {}) { + this.debug = debug; + + if (target.ownerDocument) { + this.document = target.ownerDocument; + } else if (target.baseElement) { + this.document = target.baseElement.ownerDocument; + } else { + throw new TypeError( + 'Keyboard(target): target must be DOM node or react-testing-library render() output', + ); + } + } + + toString() { + return `Keyboard{document=${this.document}, debug=${this.debug}}`; + } + + _log(message, ...args) { + if (this.debug) { + // eslint-disable-next-line no-console + console.log(`[Keyboard] ${message}`, ...args); + } + } + + _pretty(element) { + const attrs = [...element.attributes] + .map(attr => `${attr.name}="${attr.value}"`) + .join(' '); + return `<${element.nodeName.toLocaleLowerCase('en-US')} ${attrs}>`; + } + + get focused() { + return this.document.activeElement; + } + + async type(input) { + this._log( + `sending sequence '${input}' with initial focus ${this._pretty( + this.focused, + )}`, + ); + await this.send(Keyboard.fromReadableInput(input)); + } + + async send(chars) { + for (const key of chars.split('')) { + const charCode = key.charCodeAt(0); + + if (charCode === codes.Tab) { + await this.tab(); + continue; + } + + const focused = this.focused; + if (!focused || focused === this.document.body) { + throw Error( + `No element focused in document while trying to type '${Keyboard.toReadableInput( + chars, + )}'`, + ); + } + const nextValue = (focused.value || '') + key; + + if (charCode >= 32) { + await this._sendKey(key, charCode, () => { + this._log( + `sending +${key} = '${nextValue}' to ${this._pretty(focused)}`, + ); + fireEvent.change(focused, { + target: { value: nextValue }, + bubbles: true, + cancelable: true, + }); + }); + } else if (charCode === codes.Enter) { + await this.enter(focused.value || ''); + } else if (charCode === codes.Esc) { + await this.escape(); + } else if (charCode === codes.Click) { + await this.click(); + } else { + throw new Error(`Unsupported char code, ${charCode}`); + } + } + } + + async click() { + this._log(`clicking ${this._pretty(this.focused)}`); + await act(async () => fireEvent.click(this.focused)); + } + + async tab() { + await this._sendKey('Tab', codes.Tab, () => { + const focusable = this.document.querySelectorAll( + [ + 'a[href]', + 'area[href]', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + 'button:not([disabled])', + 'iframe', + 'object', + 'embed', + '*[tabindex]', + '*[contenteditable]', + ].join(','), + ); + + const tabbable = [...focusable].filter(el => { + return el.tabIndex >= 0; + }); + + const focused = this.document.activeElement; + const focusedIndex = tabbable.indexOf(focused); + const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)]; + + this._log( + `tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`, + ); + nextFocus.focus(); + }); + } + + async enter(value) { + this._log(`submitting '${value}' via ${this._pretty(this.focused)}`); + await act(() => + this._sendKey('Enter', codes.Enter, () => { + if (this.focused.type === 'button') { + fireEvent.click(this.focused, { target: { value } }); + } else { + fireEvent.submit(this.focused, { + target: { value }, + bubbles: true, + cancelable: true, + }); + } + }), + ); + } + + async escape() { + this._log(`escape from ${this._pretty(this.focused)}`); + await act(async () => this._sendKey('Escape', codes.Esc)); + } + + async _sendKey(key, charCode, action) { + const event = { key, charCode, keyCode: charCode, which: charCode }; + const focused = this.focused; + + if (fireEvent.keyDown(focused, event)) { + if (fireEvent.keyPress(focused, event)) { + if (action) { + action(); + } + } + } + fireEvent.keyUp(focused, event); + } +} diff --git a/packages/test-utils/src/testUtils/Keyboard.test.js b/packages/test-utils/src/testUtils/Keyboard.test.js new file mode 100644 index 0000000000..41ee3a12d6 --- /dev/null +++ b/packages/test-utils/src/testUtils/Keyboard.test.js @@ -0,0 +1,108 @@ +/* + * 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 React from 'react'; +import { Keyboard } from './Keyboard'; +import { render } from '@testing-library/react'; + +describe('testUtils.Keyboard', () => { + it('types into some inputs with focus and submits a form', async () => { + const typed1 = []; + const typed2 = []; + const typed3 = []; + + let submitted = false; + const handleSubmit = event => { + event.preventDefault(); + submitted = true; + }; + + const rendered = render( +
+ typed1.push(value)} /> + typed2.push(value)} + /* eslint-disable-next-line jsx-a11y/no-autofocus */ + autoFocus + /> + typed3.push(value)} /> +
, + ); + + const keyboard = new Keyboard(rendered); + await keyboard.send('xy'); + await keyboard.tab(); + await keyboard.send('abc'); + await keyboard.enter(); + + expect(typed1).toEqual([]); + expect(typed2).toEqual(['x', 'xy']); + expect(typed3).toEqual(['a', 'ab', 'abc']); + expect(submitted).toBe(true); + }); + + it('can use Keyboard.type to send readable input', async () => { + const typed1 = []; + const typed2 = []; + const typed3 = []; + + let submitted = false; + const handleSubmit = event => { + event.preventDefault(); + submitted = true; + }; + + const rendered = render( +
+ typed1.push(value)} + /> + typed2.push(value)} + /> + typed3.push(value)} + /> +
, + ); + + await Keyboard.type(rendered, ' a b c '); + + expect(typed1).toEqual(['1a']); + expect(typed2).toEqual(['2b']); + expect(typed3).toEqual(['3c']); + expect(submitted).toBe(true); + }); + + it('should be able to navigate a radio input with click', async () => { + const selections = []; + + const rendered = render( +
selections.push(value)}> + + + +
, + ); + + await Keyboard.type(rendered, ' '); + + expect(selections).toEqual(['a', 'c']); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 28585145ad..3e2fc2a01c 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -16,6 +16,11 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; +/** + * Mock implementation of {@link core-plugin-api#AnalyticsApi} with helpers to ensure that events are sent correctly. + * Use getEvents in tests to verify captured events. + * @public + */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts index 20b6bd1724..31f8aa25e3 100644 --- a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts @@ -16,11 +16,20 @@ import { ErrorApi, ErrorContext, Observable } from '@backstage/core-plugin-api'; -type Options = { +/** + * Constructor arguments for {@link MockErrorApi} + * @public + */ +export type MockErrorApiOptions = { + // Need to be true if getErrors is used in testing. collect?: boolean; }; -type ErrorWithContext = { +/** + * ErrorWithContext contains error and ErrorContext + * @public + */ +export type ErrorWithContext = { error: Error; context?: ErrorContext; }; @@ -38,11 +47,16 @@ const nullObservable = { }, }; +/** + * Mock implementation of the {@link core-plugin-api#ErrorApi} to be used in tests. + * Incudes withForError and getErrors methods for error testing. + * @public + */ export class MockErrorApi implements ErrorApi { private readonly errors = new Array(); private readonly waiters = new Set(); - constructor(private readonly options: Options = {}) {} + constructor(private readonly options: MockErrorApiOptions = {}) {} post(error: Error, context?: ErrorContext) { if (this.options.collect) { diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts index 8a6b5031e5..1273a91531 100644 --- a/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts @@ -15,3 +15,4 @@ */ export { MockErrorApi } from './MockErrorApi'; +export type { MockErrorApiOptions, ErrorWithContext } from './MockErrorApi'; diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index ab5d26afc5..3e51ed579f 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -21,8 +21,16 @@ import { } from '@backstage/core-plugin-api'; import ObservableImpl from 'zen-observable'; +/** + * Type for map holding data in {@link MockStorageApi} + * @public + */ export type MockStorageBucket = { [key: string]: any }; +/** + * Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests + * @public + */ export class MockStorageApi implements StorageApi { private readonly namespace: string; private readonly data: MockStorageBucket; diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 09c9e5d0bf..6f55cc6190 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -27,7 +27,7 @@ import { createRouteRef, } from '@backstage/core-plugin-api'; import { RenderResult } from '@testing-library/react'; -import { renderWithEffects } from '@backstage/test-utils-core'; +import { renderWithEffects } from './testingLibrary'; import { mockApis } from './mockApis'; const ErrorBoundaryFallback = ({ error }: { error: Error }) => { @@ -43,8 +43,9 @@ const Progress = () =>
; /** * Options to customize the behavior of the test app wrapper. + * @public */ -type TestAppOptions = { +export type TestAppOptions = { /** * Initial route entries to pass along as `initialEntries` to the router. */ @@ -56,11 +57,11 @@ type TestAppOptions = { * used by `useRouteRef` in the rendered elements. * * @example - * wrapInTestApp(, { - * mountedRoutes: { + * wrapInTestApp(, \{ + * mountedRoutes: \{ * '/my-path': myRouteRef, - * } - * }) + * \} + * \}) * // ... * const link = useRouteRef(myRouteRef) */ @@ -80,6 +81,7 @@ function isExternalRouteRef( * * @param Component - A component or react node to render inside the test app. * @param options - Additional options for the rendering. + * @public */ export function wrapInTestApp( Component: ComponentType | ReactNode, @@ -171,6 +173,7 @@ export function wrapInTestApp( * * @param Component - A component or react node to render inside the test app. * @param options - Additional options for the rendering. + * @public */ export async function renderInTestApp( Component: ComponentType | ReactNode, diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index 859315313d..7d93d606cc 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -17,4 +17,8 @@ export * from './apis'; export { default as mockBreakpoint } from './mockBreakpoint'; export { wrapInTestApp, renderInTestApp } from './appWrappers'; +export type { TestAppOptions } from './appWrappers'; export * from './msw'; +export * from './Keyboard'; +export * from './logCollector'; +export * from './testingLibrary'; diff --git a/packages/test-utils/src/testUtils/logCollector.test.ts b/packages/test-utils/src/testUtils/logCollector.test.ts new file mode 100644 index 0000000000..d877e4d10b --- /dev/null +++ b/packages/test-utils/src/testUtils/logCollector.test.ts @@ -0,0 +1,96 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import { withLogCollector } from './logCollector'; + +describe('logCollector', () => { + it('should collect some logs synchronously', () => { + const logs = withLogCollector(() => { + console.log('a'); + console.warn('b'); + console.error('c'); + console.error('3'); + console.warn('2'); + console.log('1'); + }); + + expect(logs.log).toEqual(['a', '1']); + expect(logs.warn).toEqual(['b', '2']); + expect(logs.error).toEqual(['c', '3']); + }); + + it('should collect some logs asynchrnously', async () => { + const logs = await withLogCollector(async () => { + console.log('a'); + console.warn('b'); + console.error('c'); + console.error('3'); + console.warn('2'); + console.log('1'); + }); + + expect(logs.log).toEqual(['a', '1']); + expect(logs.warn).toEqual(['b', '2']); + expect(logs.error).toEqual(['c', '3']); + }); + + it('should collect specific logs synchronously', () => { + const missedLogs = withLogCollector(() => { + const logs = withLogCollector(['warn', 'log'], () => { + console.log('a'); + console.warn('b'); + console.error('c'); + console.error('3'); + console.warn('2'); + console.log('1'); + }); + + expect(logs.log).toEqual(['a', '1']); + expect(logs.warn).toEqual(['b', '2']); + // @ts-ignore + expect(logs.error).toEqual([]); + }); + + expect(missedLogs.log).toEqual([]); + expect(missedLogs.warn).toEqual([]); + expect(missedLogs.error).toEqual(['c', '3']); + }); + + it('should collect specific logs asynchrnously', async () => { + const missedLogs = await withLogCollector(async () => { + const logs = await withLogCollector(['error'], async () => { + console.log('a'); + console.warn('b'); + console.error('c'); + console.error('3'); + console.warn('2'); + console.log('1'); + }); + + // @ts-ignore + expect(logs.log).toEqual([]); + // @ts-ignore + expect(logs.warn).toEqual([]); + expect(logs.error).toEqual(['c', '3']); + }); + + expect(missedLogs.log).toEqual(['a', '1']); + expect(missedLogs.warn).toEqual(['b', '2']); + expect(missedLogs.error).toEqual([]); + }); +}); diff --git a/packages/test-utils/src/testUtils/logCollector.ts b/packages/test-utils/src/testUtils/logCollector.ts new file mode 100644 index 0000000000..25629f2c13 --- /dev/null +++ b/packages/test-utils/src/testUtils/logCollector.ts @@ -0,0 +1,140 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +/** + * Severity levels of {@link CollectedLogs} + * @public */ +export type LogFuncs = 'log' | 'warn' | 'error'; +/** + * AsyncLogCollector type used in {@link (withLogCollector:1)} callback function. + * @public */ +export type AsyncLogCollector = () => Promise; +/** + * SyncLogCollector type used in {@link (withLogCollector:2)} callback function. + * @public */ +export type SyncLogCollector = () => void; +/** + * Union type used in {@link (withLogCollector:3)} callback function. + * @public */ +export type LogCollector = AsyncLogCollector | SyncLogCollector; +/** + * Map of severity level and corresponding log lines. + * @public */ +export type CollectedLogs = { [key in T]: string[] }; + +const allCategories = ['log', 'warn', 'error']; + +/** + * Asynchronous log collector with that collects all categories + * @public */ +export function withLogCollector( + callback: AsyncLogCollector, +): Promise>; + +/** + * Synchronous log collector with that collects all categories + * @public */ +export function withLogCollector( + callback: SyncLogCollector, +): CollectedLogs; + +/** + * Asynchronous log collector with that only collects selected categories + * @public + */ +export function withLogCollector( + logsToCollect: T[], + callback: AsyncLogCollector, +): Promise>; + +/** + * Synchronous log collector with that only collects selected categories + * @public */ +export function withLogCollector( + logsToCollect: T[], + callback: SyncLogCollector, +): CollectedLogs; + +/** + * Log collector that collect logs either from a sync or async collector. + * @public + * @deprecated import from test-utils instead + * */ +export function withLogCollector( + logsToCollect: LogFuncs[] | LogCollector, + callback?: LogCollector, +): CollectedLogs | Promise> { + const oneArg = !callback; + const actualCallback = (oneArg ? logsToCollect : callback) as LogCollector; + const categories = (oneArg ? allCategories : logsToCollect) as LogFuncs[]; + + const logs = { + log: new Array(), + warn: new Array(), + error: new Array(), + }; + + const origLog = console.log; + const origWarn = console.warn; + const origError = console.error; + + if (categories.includes('log')) { + console.log = (message: string) => { + logs.log.push(message); + }; + } + if (categories.includes('warn')) { + console.warn = (message: string) => { + logs.warn.push(message); + }; + } + if (categories.includes('error')) { + console.error = (message: string) => { + logs.error.push(message); + }; + } + + const restore = () => { + console.log = origLog; + console.warn = origWarn; + console.error = origError; + }; + + try { + const ret = actualCallback(); + + if (!ret || !ret.then) { + restore(); + return logs; + } + + return ret.then( + () => { + restore(); + return logs; + }, + error => { + restore(); + throw error; + }, + ); + } catch (error) { + restore(); + throw error; + } +} diff --git a/packages/test-utils/src/testUtils/mockBreakpoint.ts b/packages/test-utils/src/testUtils/mockBreakpoint.ts index 3335284f88..3bc285a81d 100644 --- a/packages/test-utils/src/testUtils/mockBreakpoint.ts +++ b/packages/test-utils/src/testUtils/mockBreakpoint.ts @@ -24,7 +24,7 @@ * If there are any updates from MUI React on testing `useMediaQuery` this mock should be replaced * https://material-ui.com/components/use-media-query/#testing * - * @param matchMediaOptions + * @public */ export default function mockBreakpoint({ matches = false }) { Object.defineProperty(window, 'matchMedia', { diff --git a/packages/test-utils/src/testUtils/testingLibrary.ts b/packages/test-utils/src/testUtils/testingLibrary.ts new file mode 100644 index 0000000000..44ea20a4cc --- /dev/null +++ b/packages/test-utils/src/testUtils/testingLibrary.ts @@ -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 { ReactElement } from 'react'; +import { act } from 'react-dom/test-utils'; +import { render, RenderResult } from '@testing-library/react'; + +/** + * @public + * Simplifies rendering of async components in by taking care of the wrapping inside act + * + * @remarks + * + * Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async + * act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit + * cleaner, since act doesn't return the result of the evaluated function. + * https://github.com/testing-library/react-testing-library/issues/281 + * https://github.com/facebook/react/pull/14853 + */ +export async function renderWithEffects( + nodes: ReactElement, +): Promise { + let value: RenderResult; + await act(async () => { + value = render(nodes); + }); + // @ts-ignore + return value; +} From 87a7cbdb6833cdd38b712eb7caaf52979f1e3e0f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:14:35 +0200 Subject: [PATCH 068/245] replace msw with setupRequestMockHandlers Signed-off-by: Johan Haals --- packages/test-utils-core/api-report.md | 2 -- packages/test-utils/api-report.md | 11 ++++++++--- .../test-utils/src/testUtils/msw/index.ts | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/test-utils-core/api-report.md b/packages/test-utils-core/api-report.md index 60d4b5b0f7..a72e5d3038 100644 --- a/packages/test-utils-core/api-report.md +++ b/packages/test-utils-core/api-report.md @@ -14,8 +14,6 @@ export type CollectedLogs = { [key in T]: string[]; }; -// Warning: (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative -// // @public @deprecated (undocumented) export class Keyboard { constructor( diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index b432141fa1..97fdbcc2d4 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -146,9 +146,7 @@ export type MockStorageBucket = { [key: string]: any; }; -// Warning: (ae-missing-release-tag) "msw" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const msw: { setupDefaultHandlers: (worker: { listen: (t: any) => void; @@ -166,6 +164,13 @@ export function renderInTestApp( // @public export function renderWithEffects(nodes: ReactElement): Promise; +// @public +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + // @public export type SyncLogCollector = () => void; diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index 337c2999fc..f4777e9ee5 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +/** + * @deprecated use {@link setupRequestMockHandlers} instead which can be called directly with the worker. + * @public + */ export const msw = { setupDefaultHandlers: (worker: { listen: (t: any) => void; @@ -25,3 +29,18 @@ export const msw = { afterEach(() => worker.resetHandlers()); }, }; + +/** + * Sets up handlers for request mocking + * @public + * @param worker - service worker + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}) { + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + afterEach(() => worker.resetHandlers()); +} From 60a75ae833acfce424040e8f1f7f3df4834a9766 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:31:42 +0200 Subject: [PATCH 069/245] chore: fix import Signed-off-by: Johan Haals --- packages/test-utils/src/testUtils/appWrappers.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index a5a60130f2..25809912ec 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -23,7 +23,7 @@ import { useApi, useRouteRef, } from '@backstage/core-plugin-api'; -import { withLogCollector } from '@backstage/test-utils-core'; +import { withLogCollector } from './logCollector'; import { render } from '@testing-library/react'; import React, { useEffect } from 'react'; import { Route, Routes } from 'react-router'; From bb12aae352eacd818a906511c50f7f89388026ea Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:31:54 +0200 Subject: [PATCH 070/245] Add changeset Signed-off-by: Johan Haals --- .changeset/dirty-bugs-care.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/dirty-bugs-care.md diff --git a/.changeset/dirty-bugs-care.md b/.changeset/dirty-bugs-care.md new file mode 100644 index 0000000000..2b7a49556b --- /dev/null +++ b/.changeset/dirty-bugs-care.md @@ -0,0 +1,10 @@ +--- +'@backstage/test-utils': patch +'@backstage/test-utils-core': patch +--- + +Migrates all utility methods from `test-utils-core` into `test-utils`. +The entire `@backstage/test-utils-core` is considered deprecated. The package will be removed in a few weeks time. +This should have almost no impact since this package is primarily used internally by core packages. + +Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. From 925a967f363df311287eeb495adfe1cd145d3034 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:49:42 +0200 Subject: [PATCH 071/245] Replace usage of test-utils-core with test-utils Signed-off-by: Johan Haals --- .changeset/rude-starfishes-walk.md | 7 +++++++ packages/config/package.json | 2 +- packages/config/src/reader.test.ts | 2 +- packages/core-app-api/package.json | 1 - packages/core-app-api/src/apis/system/ApiProvider.test.tsx | 2 +- packages/core-plugin-api/package.json | 3 +-- .../core-plugin-api/src/extensions/extensions.test.tsx | 2 +- 7 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/rude-starfishes-walk.md diff --git a/.changeset/rude-starfishes-walk.md b/.changeset/rude-starfishes-walk.md new file mode 100644 index 0000000000..87ec76fd0a --- /dev/null +++ b/.changeset/rude-starfishes-walk.md @@ -0,0 +1,7 @@ +--- +'@backstage/config': patch +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +--- + +Replace usage of test-utils-core with test-utils diff --git a/packages/config/package.json b/packages/config/package.json index ffa2404300..c5eb7a9958 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -33,7 +33,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils-core": "^0.1.2", + "@backstage/test-utils": "^0.1.19", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 0cea80dca1..3cbab17637 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { withLogCollector } from '@backstage/test-utils-core'; +import { withLogCollector } from '@backstage/test-utils'; import { ConfigReader } from './reader'; const DATA = { diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index d11b7122ce..ea76a3863d 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -47,7 +47,6 @@ "devDependencies": { "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", - "@backstage/test-utils-core": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index d1793f890e..c08674adfe 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -25,7 +25,7 @@ import { import { ApiProvider } from './ApiProvider'; import { ApiRegistry } from './ApiRegistry'; import { render } from '@testing-library/react'; -import { withLogCollector } from '@backstage/test-utils-core'; +import { withLogCollector } from '@backstage/test-utils'; import { useVersionedContext } from '@backstage/version-bridge'; describe('ApiProvider', () => { diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 471e510d88..6a9c3c7a60 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -44,8 +44,7 @@ "devDependencies": { "@backstage/cli": "^0.8.0", "@backstage/core-app-api": "^0.1.18", - "@backstage/test-utils": "^0.1.18", - "@backstage/test-utils-core": "^0.1.3", + "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/src/extensions/extensions.test.tsx b/packages/core-plugin-api/src/extensions/extensions.test.tsx index fabf90eebb..29c696c71f 100644 --- a/packages/core-plugin-api/src/extensions/extensions.test.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { withLogCollector } from '@backstage/test-utils-core'; +import { withLogCollector } from '@backstage/test-utils'; import { render, screen } from '@testing-library/react'; import React from 'react'; import { useAnalyticsContext } from '../analytics/AnalyticsContext'; From 379e1863323fdcb93049946449d465a34e4ba24a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:59:19 +0200 Subject: [PATCH 072/245] Remove all exports from test-utils-core Signed-off-by: Johan Haals --- packages/test-utils-core/api-report.md | 91 +------ packages/test-utils-core/src/index.ts | 11 +- packages/test-utils-core/src/setupTests.ts | 17 -- .../test-utils-core/src/testUtils/Keyboard.js | 224 ------------------ .../src/testUtils/Keyboard.test.js | 108 --------- .../test-utils-core/src/testUtils/index.tsx | 19 -- .../src/testUtils/logCollector.test.ts | 96 -------- .../src/testUtils/logCollector.ts | 144 ----------- .../src/testUtils/testingLibrary.ts | 41 ---- 9 files changed, 5 insertions(+), 746 deletions(-) delete mode 100644 packages/test-utils-core/src/setupTests.ts delete mode 100644 packages/test-utils-core/src/testUtils/Keyboard.js delete mode 100644 packages/test-utils-core/src/testUtils/Keyboard.test.js delete mode 100644 packages/test-utils-core/src/testUtils/index.tsx delete mode 100644 packages/test-utils-core/src/testUtils/logCollector.test.ts delete mode 100644 packages/test-utils-core/src/testUtils/logCollector.ts delete mode 100644 packages/test-utils-core/src/testUtils/testingLibrary.ts diff --git a/packages/test-utils-core/api-report.md b/packages/test-utils-core/api-report.md index a72e5d3038..f21c749727 100644 --- a/packages/test-utils-core/api-report.md +++ b/packages/test-utils-core/api-report.md @@ -3,94 +3,5 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { ReactElement } from 'react'; -import { RenderResult } from '@testing-library/react'; - -// @public @deprecated (undocumented) -export type AsyncLogCollector = () => Promise; - -// @public @deprecated (undocumented) -export type CollectedLogs = { - [key in T]: string[]; -}; - -// @public @deprecated (undocumented) -export class Keyboard { - constructor( - target: any, - { - debug, - }?: { - debug?: boolean | undefined; - }, - ); - // (undocumented) - click(): Promise; - // (undocumented) - debug: boolean; - // (undocumented) - document: any; - // (undocumented) - enter(value: any): Promise; - // (undocumented) - escape(): Promise; - // (undocumented) - get focused(): any; - // (undocumented) - static fromReadableInput(input: any): any; - // (undocumented) - _log(message: any, ...args: any[]): void; - // (undocumented) - _pretty(element: any): string; - // (undocumented) - send(chars: any): Promise; - // (undocumented) - _sendKey(key: any, charCode: any, action: any): Promise; - // (undocumented) - tab(): Promise; - // (undocumented) - static toReadableInput(chars: any): any; - // (undocumented) - toString(): string; - // (undocumented) - static type(target: any, input: any): Promise; - // (undocumented) - type(input: any): Promise; - // (undocumented) - static typeDebug(target: any, input: any): Promise; -} - -// @public @deprecated (undocumented) -export type LogCollector = AsyncLogCollector | SyncLogCollector; - -// @public @deprecated (undocumented) -export type LogFuncs = 'log' | 'warn' | 'error'; - -// @public @deprecated -export function renderWithEffects(nodes: ReactElement): Promise; - -// @public @deprecated (undocumented) -export type SyncLogCollector = () => void; - -// @public @deprecated -export function withLogCollector( - callback: AsyncLogCollector, -): Promise>; - -// @public @deprecated -export function withLogCollector( - callback: SyncLogCollector, -): CollectedLogs; - -// @public @deprecated -export function withLogCollector( - logsToCollect: T[], - callback: AsyncLogCollector, -): Promise>; - -// @public @deprecated -export function withLogCollector( - logsToCollect: T[], - callback: SyncLogCollector, -): CollectedLogs; +// (No @packageDocumentation comment for this package) ``` diff --git a/packages/test-utils-core/src/index.ts b/packages/test-utils-core/src/index.ts index 5658523c11..8f0a5b31e1 100644 --- a/packages/test-utils-core/src/index.ts +++ b/packages/test-utils-core/src/index.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -/** - * Utilities to test Backstage core - * - * @packageDocumentation - */ - -export * from './testUtils'; +export {}; +throw new Error( + 'This module has been removed. Use @backstage/dev-utils instead', +); diff --git a/packages/test-utils-core/src/setupTests.ts b/packages/test-utils-core/src/setupTests.ts deleted file mode 100644 index 963c0f188b..0000000000 --- a/packages/test-utils-core/src/setupTests.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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'; diff --git a/packages/test-utils-core/src/testUtils/Keyboard.js b/packages/test-utils-core/src/testUtils/Keyboard.js deleted file mode 100644 index b0600eb78d..0000000000 --- a/packages/test-utils-core/src/testUtils/Keyboard.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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 { act, fireEvent } from '@testing-library/react'; - -const codes = { - Tab: 9, - Enter: 10, - Click: 17 /* This keyboard can click, deal with it */, - Esc: 27, -}; - -/** - * @public - * @deprecated because of no usages. */ -export class Keyboard { - static async type(target, input) { - await new Keyboard(target).type(input); - } - - static async typeDebug(target, input) { - await new Keyboard(target, { debug: true }).type(input); - } - - static toReadableInput(chars) { - return chars.split('').map(char => { - switch (char.charCodeAt(0)) { - case codes.Tab: - return ''; - case codes.Enter: - return ''; - case codes.Click: - return ''; - case codes.Esc: - return ''; - default: - return char; - } - }); - } - - static fromReadableInput(input) { - return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => { - if (name in codes) { - return String.fromCharCode(codes[name]); - } - throw new Error(`Unknown char name: '${name}'`); - }); - } - - constructor(target, { debug = false } = {}) { - this.debug = debug; - - if (target.ownerDocument) { - this.document = target.ownerDocument; - } else if (target.baseElement) { - this.document = target.baseElement.ownerDocument; - } else { - throw new TypeError( - 'Keyboard(target): target must be DOM node or react-testing-library render() output', - ); - } - } - - toString() { - return `Keyboard{document=${this.document}, debug=${this.debug}}`; - } - - _log(message, ...args) { - if (this.debug) { - // eslint-disable-next-line no-console - console.log(`[Keyboard] ${message}`, ...args); - } - } - - _pretty(element) { - const attrs = [...element.attributes] - .map(attr => `${attr.name}="${attr.value}"`) - .join(' '); - return `<${element.nodeName.toLocaleLowerCase('en-US')} ${attrs}>`; - } - - get focused() { - return this.document.activeElement; - } - - async type(input) { - this._log( - `sending sequence '${input}' with initial focus ${this._pretty( - this.focused, - )}`, - ); - await this.send(Keyboard.fromReadableInput(input)); - } - - async send(chars) { - for (const key of chars.split('')) { - const charCode = key.charCodeAt(0); - - if (charCode === codes.Tab) { - await this.tab(); - continue; - } - - const focused = this.focused; - if (!focused || focused === this.document.body) { - throw Error( - `No element focused in document while trying to type '${Keyboard.toReadableInput( - chars, - )}'`, - ); - } - const nextValue = (focused.value || '') + key; - - if (charCode >= 32) { - await this._sendKey(key, charCode, () => { - this._log( - `sending +${key} = '${nextValue}' to ${this._pretty(focused)}`, - ); - fireEvent.change(focused, { - target: { value: nextValue }, - bubbles: true, - cancelable: true, - }); - }); - } else if (charCode === codes.Enter) { - await this.enter(focused.value || ''); - } else if (charCode === codes.Esc) { - await this.escape(); - } else if (charCode === codes.Click) { - await this.click(); - } else { - throw new Error(`Unsupported char code, ${charCode}`); - } - } - } - - async click() { - this._log(`clicking ${this._pretty(this.focused)}`); - await act(async () => fireEvent.click(this.focused)); - } - - async tab() { - await this._sendKey('Tab', codes.Tab, () => { - const focusable = this.document.querySelectorAll( - [ - 'a[href]', - 'area[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - 'button:not([disabled])', - 'iframe', - 'object', - 'embed', - '*[tabindex]', - '*[contenteditable]', - ].join(','), - ); - - const tabbable = [...focusable].filter(el => { - return el.tabIndex >= 0; - }); - - const focused = this.document.activeElement; - const focusedIndex = tabbable.indexOf(focused); - const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)]; - - this._log( - `tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`, - ); - nextFocus.focus(); - }); - } - - async enter(value) { - this._log(`submitting '${value}' via ${this._pretty(this.focused)}`); - await act(() => - this._sendKey('Enter', codes.Enter, () => { - if (this.focused.type === 'button') { - fireEvent.click(this.focused, { target: { value } }); - } else { - fireEvent.submit(this.focused, { - target: { value }, - bubbles: true, - cancelable: true, - }); - } - }), - ); - } - - async escape() { - this._log(`escape from ${this._pretty(this.focused)}`); - await act(async () => this._sendKey('Escape', codes.Esc)); - } - - async _sendKey(key, charCode, action) { - const event = { key, charCode, keyCode: charCode, which: charCode }; - const focused = this.focused; - - if (fireEvent.keyDown(focused, event)) { - if (fireEvent.keyPress(focused, event)) { - if (action) { - action(); - } - } - } - fireEvent.keyUp(focused, event); - } -} diff --git a/packages/test-utils-core/src/testUtils/Keyboard.test.js b/packages/test-utils-core/src/testUtils/Keyboard.test.js deleted file mode 100644 index 41ee3a12d6..0000000000 --- a/packages/test-utils-core/src/testUtils/Keyboard.test.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 React from 'react'; -import { Keyboard } from './Keyboard'; -import { render } from '@testing-library/react'; - -describe('testUtils.Keyboard', () => { - it('types into some inputs with focus and submits a form', async () => { - const typed1 = []; - const typed2 = []; - const typed3 = []; - - let submitted = false; - const handleSubmit = event => { - event.preventDefault(); - submitted = true; - }; - - const rendered = render( -
- typed1.push(value)} /> - typed2.push(value)} - /* eslint-disable-next-line jsx-a11y/no-autofocus */ - autoFocus - /> - typed3.push(value)} /> -
, - ); - - const keyboard = new Keyboard(rendered); - await keyboard.send('xy'); - await keyboard.tab(); - await keyboard.send('abc'); - await keyboard.enter(); - - expect(typed1).toEqual([]); - expect(typed2).toEqual(['x', 'xy']); - expect(typed3).toEqual(['a', 'ab', 'abc']); - expect(submitted).toBe(true); - }); - - it('can use Keyboard.type to send readable input', async () => { - const typed1 = []; - const typed2 = []; - const typed3 = []; - - let submitted = false; - const handleSubmit = event => { - event.preventDefault(); - submitted = true; - }; - - const rendered = render( -
- typed1.push(value)} - /> - typed2.push(value)} - /> - typed3.push(value)} - /> -
, - ); - - await Keyboard.type(rendered, ' a b c '); - - expect(typed1).toEqual(['1a']); - expect(typed2).toEqual(['2b']); - expect(typed3).toEqual(['3c']); - expect(submitted).toBe(true); - }); - - it('should be able to navigate a radio input with click', async () => { - const selections = []; - - const rendered = render( -
selections.push(value)}> - - - -
, - ); - - await Keyboard.type(rendered, ' '); - - expect(selections).toEqual(['a', 'c']); - }); -}); diff --git a/packages/test-utils-core/src/testUtils/index.tsx b/packages/test-utils-core/src/testUtils/index.tsx deleted file mode 100644 index 6f6aa43ea9..0000000000 --- a/packages/test-utils-core/src/testUtils/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 * from './Keyboard'; -export * from './logCollector'; -export * from './testingLibrary'; diff --git a/packages/test-utils-core/src/testUtils/logCollector.test.ts b/packages/test-utils-core/src/testUtils/logCollector.test.ts deleted file mode 100644 index d877e4d10b..0000000000 --- a/packages/test-utils-core/src/testUtils/logCollector.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable no-console */ - -import { withLogCollector } from './logCollector'; - -describe('logCollector', () => { - it('should collect some logs synchronously', () => { - const logs = withLogCollector(() => { - console.log('a'); - console.warn('b'); - console.error('c'); - console.error('3'); - console.warn('2'); - console.log('1'); - }); - - expect(logs.log).toEqual(['a', '1']); - expect(logs.warn).toEqual(['b', '2']); - expect(logs.error).toEqual(['c', '3']); - }); - - it('should collect some logs asynchrnously', async () => { - const logs = await withLogCollector(async () => { - console.log('a'); - console.warn('b'); - console.error('c'); - console.error('3'); - console.warn('2'); - console.log('1'); - }); - - expect(logs.log).toEqual(['a', '1']); - expect(logs.warn).toEqual(['b', '2']); - expect(logs.error).toEqual(['c', '3']); - }); - - it('should collect specific logs synchronously', () => { - const missedLogs = withLogCollector(() => { - const logs = withLogCollector(['warn', 'log'], () => { - console.log('a'); - console.warn('b'); - console.error('c'); - console.error('3'); - console.warn('2'); - console.log('1'); - }); - - expect(logs.log).toEqual(['a', '1']); - expect(logs.warn).toEqual(['b', '2']); - // @ts-ignore - expect(logs.error).toEqual([]); - }); - - expect(missedLogs.log).toEqual([]); - expect(missedLogs.warn).toEqual([]); - expect(missedLogs.error).toEqual(['c', '3']); - }); - - it('should collect specific logs asynchrnously', async () => { - const missedLogs = await withLogCollector(async () => { - const logs = await withLogCollector(['error'], async () => { - console.log('a'); - console.warn('b'); - console.error('c'); - console.error('3'); - console.warn('2'); - console.log('1'); - }); - - // @ts-ignore - expect(logs.log).toEqual([]); - // @ts-ignore - expect(logs.warn).toEqual([]); - expect(logs.error).toEqual(['c', '3']); - }); - - expect(missedLogs.log).toEqual(['a', '1']); - expect(missedLogs.warn).toEqual(['b', '2']); - expect(missedLogs.error).toEqual([]); - }); -}); diff --git a/packages/test-utils-core/src/testUtils/logCollector.ts b/packages/test-utils-core/src/testUtils/logCollector.ts deleted file mode 100644 index 22f06e172b..0000000000 --- a/packages/test-utils-core/src/testUtils/logCollector.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable no-console */ - -/** - * @public - * @deprecated import from test-utils instead */ -export type LogFuncs = 'log' | 'warn' | 'error'; -/** - * @public - * @deprecated import from test-utils instead */ -export type AsyncLogCollector = () => Promise; -/** - * @public - * @deprecated import from test-utils instead */ -export type SyncLogCollector = () => void; -/** - * @public - * @deprecated import from test-utils instead */ -export type LogCollector = AsyncLogCollector | SyncLogCollector; -/** - * @public - * @deprecated import from test-utils instead */ -export type CollectedLogs = { [key in T]: string[] }; - -const allCategories = ['log', 'warn', 'error']; - -/** - * Asynchronous log collector with that collects all categories - * @public - * @deprecated import from test-utils instead */ -export function withLogCollector( - callback: AsyncLogCollector, -): Promise>; - -/** - * Synchronous log collector with that collects all categories - * @public - * @deprecated import from test-utils instead */ -export function withLogCollector( - callback: SyncLogCollector, -): CollectedLogs; - -/** - * Asynchronous log collector with that only collects selected categories - * @public - * @deprecated import from test-utils instead - */ - -export function withLogCollector( - logsToCollect: T[], - callback: AsyncLogCollector, -): Promise>; - -/** - * Synchronous log collector with that only collects selected categories - * @public - * @deprecated import from test-utils instead */ -export function withLogCollector( - logsToCollect: T[], - callback: SyncLogCollector, -): CollectedLogs; - -/** - * @public - * @deprecated import from test-utils instead - * */ -export function withLogCollector( - logsToCollect: LogFuncs[] | LogCollector, - callback?: LogCollector, -): CollectedLogs | Promise> { - const oneArg = !callback; - const actualCallback = (oneArg ? logsToCollect : callback) as LogCollector; - const categories = (oneArg ? allCategories : logsToCollect) as LogFuncs[]; - - const logs = { - log: new Array(), - warn: new Array(), - error: new Array(), - }; - - const origLog = console.log; - const origWarn = console.warn; - const origError = console.error; - - if (categories.includes('log')) { - console.log = (message: string) => { - logs.log.push(message); - }; - } - if (categories.includes('warn')) { - console.warn = (message: string) => { - logs.warn.push(message); - }; - } - if (categories.includes('error')) { - console.error = (message: string) => { - logs.error.push(message); - }; - } - - const restore = () => { - console.log = origLog; - console.warn = origWarn; - console.error = origError; - }; - - try { - const ret = actualCallback(); - - if (!ret || !ret.then) { - restore(); - return logs; - } - - return ret.then( - () => { - restore(); - return logs; - }, - error => { - restore(); - throw error; - }, - ); - } catch (error) { - restore(); - throw error; - } -} diff --git a/packages/test-utils-core/src/testUtils/testingLibrary.ts b/packages/test-utils-core/src/testUtils/testingLibrary.ts deleted file mode 100644 index 953a414968..0000000000 --- a/packages/test-utils-core/src/testUtils/testingLibrary.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import { render, RenderResult } from '@testing-library/react'; - -/** - * Simplifies rendering of async components in by taking care of the wrapping inside act - * @remarks - * Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async - * act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit - * cleaner, since act doesn't return the result of the evaluated function. - * https://github.com/testing-library/react-testing-library/issues/281 - * https://github.com/facebook/react/pull/14853 - * @public - * @deprecated import from test-utils instead - */ -export async function renderWithEffects( - nodes: ReactElement, -): Promise { - let value: RenderResult; - await act(async () => { - value = render(nodes); - }); - // @ts-ignore - return value; -} From c976066b05017b472fc09fb98b916cd0c5a3889e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:59:42 +0200 Subject: [PATCH 073/245] Update wording in changeset Signed-off-by: Johan Haals --- .changeset/dirty-bugs-care.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/dirty-bugs-care.md b/.changeset/dirty-bugs-care.md index 2b7a49556b..4d90a9c939 100644 --- a/.changeset/dirty-bugs-care.md +++ b/.changeset/dirty-bugs-care.md @@ -3,8 +3,7 @@ '@backstage/test-utils-core': patch --- -Migrates all utility methods from `test-utils-core` into `test-utils`. -The entire `@backstage/test-utils-core` is considered deprecated. The package will be removed in a few weeks time. -This should have almost no impact since this package is primarily used internally by core packages. +Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. +This should have no impact since this package is considered internal and have no usages outside core packages. Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. From adb27441e6be45f7fb60feab3f1f245151da6ae5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 14:59:57 +0200 Subject: [PATCH 074/245] use setupRequestMockHandlers inside msw Signed-off-by: Johan Haals --- packages/test-utils/src/testUtils/msw/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index f4777e9ee5..40bc47e8f0 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -24,9 +24,7 @@ export const msw = { close: () => void; resetHandlers: () => void; }) => { - beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); - afterAll(() => worker.close()); - afterEach(() => worker.resetHandlers()); + setupRequestMockHandlers(worker); }, }; From 1ed3eb452774ba2ceaeac97b4a1dddf9575cce62 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 15:28:26 +0200 Subject: [PATCH 075/245] remove unused dependencies Signed-off-by: Johan Haals --- packages/test-utils-core/package.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 474d62a670..a5b3df3ff3 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -28,13 +28,7 @@ "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" }, - "dependencies": { - "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react": "^11.2.5", - "@types/react": "*", - "react": "^16.12.0", - "react-dom": "^16.12.0" - }, + "dependencies": {}, "devDependencies": { "@types/jest": "^26.0.7", "@types/node": "^14.14.32" From 76a85113bc91eb29b8c117cb14ffc020ce40f06f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 21 Oct 2021 15:55:46 +0200 Subject: [PATCH 076/245] pass with no tests Signed-off-by: Johan Haals --- packages/test-utils-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index a5b3df3ff3..8256714560 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "backstage-cli build --outputs types,esm", "lint": "backstage-cli lint", - "test": "backstage-cli test", + "test": "backstage-cli test --passWithNoTests", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" From d4d07abec09d7a4ca04456dc94464a7fe8edc7b2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 22 Oct 2021 10:39:13 +0200 Subject: [PATCH 077/245] types: fix cli dep version query Signed-off-by: Patrik Oldsberg --- packages/types/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/package.json b/packages/types/package.json index e8117fff37..9da8cd5e95 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -31,7 +31,7 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.8.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, From 3b767f19c97ecfa436f3e578121d69d3207890c9 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Fri, 1 Oct 2021 09:39:55 +0100 Subject: [PATCH 078/245] Adopt extra field for OAuth state Currently, the OAuth state is very limited. It only accepts three field: * env * nonce * origin This does not give the user much flexibility when passing in other fields to the state. Origin is set based on the window location. Env determined by the running environment of backstage. Nonce, randomly generated every time. If a user wanted to verify other fields in the state, they would be unable to do so. For example, let's say you have a GitHub app that serves multiple installations. In order for this to work you need a middle service between github and backstage. This service needs to programaticaly determine where to redirect the requests to (GitHub apps only allow one redirect url). Your intermediate service requires you to redirect to other paths on backstage based on the type of request the Github ap p receives. By adding in the `extraState` to the Github Provider Options, this can now be achieved. You can set the field to `{'redirect_url': '/some/path/to/redirect/to'}` to complete the OAuth flow. Although this is a very specific use case, I believe this will be useful across all the providers. Signed-off-by: Nicolas Arnold --- .changeset/lucky-books-worry.md | 5 ++++ plugins/auth-backend/api-report.md | 5 +++- .../src/lib/oauth/helpers.test.ts | 26 ++++++++++++++++--- plugins/auth-backend/src/lib/oauth/types.ts | 2 +- .../src/providers/github/provider.ts | 13 +++++++++- 5 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 .changeset/lucky-books-worry.md diff --git a/.changeset/lucky-books-worry.md b/.changeset/lucky-books-worry.md new file mode 100644 index 0000000000..f91dccb3f9 --- /dev/null +++ b/.changeset/lucky-books-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Allow extra field in OAuth state parameter diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 572f182082..25cd839b1d 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -290,6 +290,9 @@ export type GithubOAuthResult = { // @public (undocumented) export type GithubProviderOptions = { authHandler?: AuthHandler; + extraState?: { + [key: string]: string; + }; signIn?: { resolver?: SignInResolver; }; @@ -488,7 +491,7 @@ export type OAuthStartRequest = express.Request<{}> & { export type OAuthState = { nonce: string; env: string; - origin?: string; + [key: string]: string; }; // Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts index 8af1d2f2ad..bd667dca73 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -20,16 +20,31 @@ import { verifyNonce, encodeState, readState } from './helpers'; describe('OAuthProvider Utils', () => { describe('encodeState', () => { it('should serialized values', () => { + const state = { + nonce: '123', + env: 'development', + }; + + const encoded = encodeState(state); + expect(encoded).toBe( + Buffer.from('nonce=123&env=development').toString('hex'), + ); + + expect(readState(encoded)).toEqual(state); + }); + + it('should serialized values with extra values', () => { const state = { nonce: '123', env: 'development', origin: 'https://example.com', + redirect_url: 'https://someurl.com/foo/bar', }; const encoded = encodeState(state); expect(encoded).toBe( Buffer.from( - 'nonce=123&env=development&origin=https%3A%2F%2Fexample.com', + 'nonce=123&env=development&origin=https%3A%2F%2Fexample.com&redirect_url=https%3A%2F%2Fsomeurl.com%2Ffoo%2Fbar', ).toString('hex'), ); @@ -37,9 +52,12 @@ describe('OAuthProvider Utils', () => { }); it('should not include undefined values', () => { - const state = { nonce: '123', env: 'development', origin: undefined }; - - const encoded = encodeState(state); + const state = { + nonce: '123', + env: 'development', + }; + // @ts-ignore + const encoded = encodeState({ test: undefined, ...state }); expect(encoded).toBe( Buffer.from('nonce=123&env=development').toString('hex'), ); diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 7912dd16a5..775c5e35d1 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -77,7 +77,7 @@ export type OAuthState = { */ nonce: string; env: string; - origin?: string; + [key: string]: string; }; export type OAuthStartRequest = express.Request<{}> & { diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 82d231edfa..12603e8230 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -64,6 +64,7 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; userProfileUrl?: string; authorizationUrl?: string; + extraState?: { [key: string]: string }; signInResolver?: SignInResolver; authHandler: AuthHandler; tokenIssuer: TokenIssuer; @@ -78,12 +79,14 @@ export class GithubAuthProvider implements OAuthHandlers { private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; + private readonly extraState: { [key: string]: string }; constructor(options: GithubAuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; this.tokenIssuer = options.tokenIssuer; this.catalogIdentityClient = options.catalogIdentityClient; + this.extraState = options.extraState || {}; this.logger = options.logger; this._strategy = new GithubStrategy( { @@ -109,7 +112,7 @@ export class GithubAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, - state: encodeState(req.state), + state: encodeState({ ...this.extraState, ...req.state }), }); } @@ -200,6 +203,11 @@ export type GithubProviderOptions = { */ authHandler?: AuthHandler; + /** + * The extra state you would like to pass into the OAuth state + */ + extraState?: { [key: string]: string }; + /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ @@ -263,6 +271,8 @@ export const createGithubProvider = ( logger, }); + const extraState = options?.extraState ? options.extraState : undefined; + const provider = new GithubAuthProvider({ clientId, clientSecret, @@ -274,6 +284,7 @@ export const createGithubProvider = ( authHandler, tokenIssuer, catalogIdentityClient, + extraState, logger, }); From f86173221c5929057e50f5aa91909e0a433652f2 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 13 Oct 2021 17:49:04 +0100 Subject: [PATCH 079/245] Add callback to allow users to override state This is a slightly different implementation. It allows the user to pass in a reference to a callback so that the state can be set. It was a suggestion from @Rugvip on a discussion we had offline. The callback is an async function that returns a Promise Note: due to the way the OAuthAdapter works, this callback must include an env + nonce. Without them, your oauth request will fail. Signed-off-by: Nicolas Arnold --- plugins/auth-backend/api-report.md | 7 +- .../src/lib/oauth/helpers.test.ts | 26 +- plugins/auth-backend/src/lib/oauth/types.ts | 2 +- .../src/providers/github/provider.test.ts | 4 + .../src/providers/github/provider.ts | 27 +- plugins/auth-backend/src/providers/types.ts | 2 + yarn.lock | 307 +++++++++++++++++- 7 files changed, 336 insertions(+), 39 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 25cd839b1d..d8275ef5d0 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -290,12 +290,10 @@ export type GithubOAuthResult = { // @public (undocumented) export type GithubProviderOptions = { authHandler?: AuthHandler; - extraState?: { - [key: string]: string; - }; signIn?: { resolver?: SignInResolver; }; + stateHandler?: StateHandler; }; // Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -491,7 +489,7 @@ export type OAuthStartRequest = express.Request<{}> & { export type OAuthState = { nonce: string; env: string; - [key: string]: string; + origin?: string; }; // Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -585,6 +583,7 @@ export type WebMessageResponse = // src/providers/atlassian/provider.d.ts:37:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/atlassian/provider.d.ts:42:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts +// src/providers/github/provider.d.ts:65:5 - (ae-forgotten-export) The symbol "StateHandler" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:121:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts index bd667dca73..8af1d2f2ad 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -20,31 +20,16 @@ import { verifyNonce, encodeState, readState } from './helpers'; describe('OAuthProvider Utils', () => { describe('encodeState', () => { it('should serialized values', () => { - const state = { - nonce: '123', - env: 'development', - }; - - const encoded = encodeState(state); - expect(encoded).toBe( - Buffer.from('nonce=123&env=development').toString('hex'), - ); - - expect(readState(encoded)).toEqual(state); - }); - - it('should serialized values with extra values', () => { const state = { nonce: '123', env: 'development', origin: 'https://example.com', - redirect_url: 'https://someurl.com/foo/bar', }; const encoded = encodeState(state); expect(encoded).toBe( Buffer.from( - 'nonce=123&env=development&origin=https%3A%2F%2Fexample.com&redirect_url=https%3A%2F%2Fsomeurl.com%2Ffoo%2Fbar', + 'nonce=123&env=development&origin=https%3A%2F%2Fexample.com', ).toString('hex'), ); @@ -52,12 +37,9 @@ describe('OAuthProvider Utils', () => { }); it('should not include undefined values', () => { - const state = { - nonce: '123', - env: 'development', - }; - // @ts-ignore - const encoded = encodeState({ test: undefined, ...state }); + const state = { nonce: '123', env: 'development', origin: undefined }; + + const encoded = encodeState(state); expect(encoded).toBe( Buffer.from('nonce=123&env=development').toString('hex'), ); diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 775c5e35d1..7912dd16a5 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -77,7 +77,7 @@ export type OAuthState = { */ nonce: string; env: string; - [key: string]: string; + origin?: string; }; export type OAuthStartRequest = express.Request<{}> & { diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index d224e8d723..dd0910163f 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -25,6 +25,7 @@ import { } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; +import { OAuthStartRequest, encodeState } from '../../lib/oauth'; const mockFrameHandler = jest.spyOn( helpers, @@ -56,6 +57,9 @@ describe('GithubAuthProvider', () => { authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), + stateHandler: async (req: OAuthStartRequest) => { + return encodeState(req.state); + }, callbackUrl: 'mock', clientId: 'mock', clientSecret: 'mock', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 12603e8230..63d2cf79c9 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -31,6 +31,7 @@ import { AuthProviderFactory, AuthHandler, SignInResolver, + StateHandler, } from '../types'; import { OAuthAdapter, @@ -64,9 +65,9 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; userProfileUrl?: string; authorizationUrl?: string; - extraState?: { [key: string]: string }; signInResolver?: SignInResolver; authHandler: AuthHandler; + stateHandler: StateHandler; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; logger: Logger; @@ -79,14 +80,14 @@ export class GithubAuthProvider implements OAuthHandlers { private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; - private readonly extraState: { [key: string]: string }; + private readonly stateHandler: StateHandler; constructor(options: GithubAuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; + this.stateHandler = options.stateHandler; this.tokenIssuer = options.tokenIssuer; this.catalogIdentityClient = options.catalogIdentityClient; - this.extraState = options.extraState || {}; this.logger = options.logger; this._strategy = new GithubStrategy( { @@ -112,7 +113,7 @@ export class GithubAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, - state: encodeState({ ...this.extraState, ...req.state }), + state: await this.stateHandler(req.state), }); } @@ -203,11 +204,6 @@ export type GithubProviderOptions = { */ authHandler?: AuthHandler; - /** - * The extra state you would like to pass into the OAuth state - */ - extraState?: { [key: string]: string }; - /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ @@ -217,6 +213,11 @@ export type GithubProviderOptions = { */ resolver?: SignInResolver; }; + + /** + * The state handler that sets the uri query param 'state' + */ + stateHandler?: StateHandler; }; export const createGithubProvider = ( @@ -271,7 +272,11 @@ export const createGithubProvider = ( logger, }); - const extraState = options?.extraState ? options.extraState : undefined; + const stateHandler: StateHandler = options?.stateHandler + ? options.stateHandler + : async (req: OAuthStartRequest) => { + return encodeState(req.state); + }; const provider = new GithubAuthProvider({ clientId, @@ -284,7 +289,7 @@ export const createGithubProvider = ( authHandler, tokenIssuer, catalogIdentityClient, - extraState, + stateHandler, logger, }); diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 7f7b841c68..83e6e0bcb6 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -223,3 +223,5 @@ export type AuthHandlerResult = { profile: ProfileInfo }; export type AuthHandler = ( input: AuthResult, ) => Promise; + +export type StateHandler = (input: any) => Promise; diff --git a/yarn.lock b/yarn.lock index c905fbea6f..b38d8cef4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -343,6 +343,13 @@ dependencies: "@babel/highlight" "^7.14.5" +"@babel/code-frame@^7.15.8": + version "7.15.8" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503" + integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg== + dependencies: + "@babel/highlight" "^7.14.5" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.4": version "7.14.4" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz#45720fe0cecf3fd42019e1d12cc3d27fadc98d58" @@ -353,6 +360,11 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.9.tgz#ac7996ceaafcf8f410119c8af0d1db4cf914a210" integrity sha512-p3QjZmMGHDGdpcwEYYWu7i7oJShJvtgMjJeb0W95PPhSm++3lm8YXYOh45Y6iCN9PkZLTZ7CIX5nFrp7pw7TXw== +"@babel/compat-data@^7.15.0": + version "7.15.0" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" + integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA== + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -396,6 +408,27 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/core@^7.4.4": + version "7.15.8" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz#195b9f2bffe995d2c6c159e72fe525b4114e8c10" + integrity sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og== + dependencies: + "@babel/code-frame" "^7.15.8" + "@babel/generator" "^7.15.8" + "@babel/helper-compilation-targets" "^7.15.4" + "@babel/helper-module-transforms" "^7.15.8" + "@babel/helpers" "^7.15.4" + "@babel/parser" "^7.15.8" + "@babel/template" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.6" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + "@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.14.8", "@babel/generator@^7.14.9": version "7.14.9" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.14.9.tgz#23b19c597d38b4f7dc2e3fe42a69c88d9ecfaa16" @@ -414,6 +447,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.15.4", "@babel/generator@^7.15.8": + version "7.15.8" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz#fa56be6b596952ceb231048cf84ee499a19c0cd1" + integrity sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g== + dependencies: + "@babel/types" "^7.15.6" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -464,6 +506,16 @@ browserslist "^4.16.6" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz#cf6d94f30fbefc139123e27dd6b02f65aeedb7b9" + integrity sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ== + dependencies: + "@babel/compat-data" "^7.15.0" + "@babel/helper-validator-option" "^7.14.5" + browserslist "^4.16.6" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.14.0", "@babel/helper-create-class-features-plugin@^7.14.3": version "7.14.4" resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz#abf888d836a441abee783c75229279748705dc42" @@ -564,6 +616,15 @@ "@babel/template" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/helper-function-name@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc" + integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw== + dependencies: + "@babel/helper-get-function-arity" "^7.15.4" + "@babel/template" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helper-get-function-arity@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" @@ -578,6 +639,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-get-function-arity@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" + integrity sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-hoist-variables@^7.13.0": version "7.13.16" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.16.tgz#1b1651249e94b51f8f0d33439843e33e39775b30" @@ -593,6 +661,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-hoist-variables@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df" + integrity sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-member-expression-to-functions@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72" @@ -607,6 +682,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-member-expression-to-functions@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz#bfd34dc9bba9824a4658b0317ec2fd571a51e6ef" + integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" @@ -621,6 +703,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-module-imports@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz#e18007d230632dea19b47853b984476e7b4e103f" + integrity sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.14.8": version "7.14.8" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49" @@ -649,6 +738,20 @@ "@babel/traverse" "^7.14.2" "@babel/types" "^7.14.2" +"@babel/helper-module-transforms@^7.15.4", "@babel/helper-module-transforms@^7.15.8": + version "7.15.8" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz#d8c0e75a87a52e374a8f25f855174786a09498b2" + integrity sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg== + dependencies: + "@babel/helper-module-imports" "^7.15.4" + "@babel/helper-replace-supers" "^7.15.4" + "@babel/helper-simple-access" "^7.15.4" + "@babel/helper-split-export-declaration" "^7.15.4" + "@babel/helper-validator-identifier" "^7.15.7" + "@babel/template" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.6" + "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" @@ -663,6 +766,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-optimise-call-expression@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171" + integrity sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-plugin-utils@7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" @@ -716,6 +826,16 @@ "@babel/traverse" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/helper-replace-supers@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz#52a8ab26ba918c7f6dee28628b07071ac7b7347a" + integrity sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.15.4" + "@babel/helper-optimise-call-expression" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helper-simple-access@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" @@ -730,6 +850,13 @@ dependencies: "@babel/types" "^7.14.8" +"@babel/helper-simple-access@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz#ac368905abf1de8e9781434b635d8f8674bcc13b" + integrity sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-skip-transparent-expression-wrappers@^7.12.1": version "7.12.1" resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" @@ -758,6 +885,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-split-export-declaration@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz#aecab92dcdbef6a10aa3b62ab204b085f776e257" + integrity sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.0": version "7.14.0" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" @@ -768,6 +902,11 @@ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== +"@babel/helper-validator-identifier@^7.15.7": + version "7.15.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -807,6 +946,15 @@ "@babel/traverse" "^7.14.8" "@babel/types" "^7.14.8" +"@babel/helpers@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz#5f40f02050a3027121a3cf48d497c05c555eaf43" + integrity sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ== + dependencies: + "@babel/template" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": version "7.14.0" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" @@ -835,6 +983,11 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.14.9.tgz#596c1ad67608070058ebf8df50c1eaf65db895a4" integrity sha512-RdUTOseXJ8POjjOeEBEvNMIZU/nm4yu2rufRkcibzkkg7DmQvXU8v3M4Xk9G7uuI86CDGkKcuDWgioqZm+mScQ== +"@babel/parser@^7.15.4", "@babel/parser@^7.15.8": + version "7.15.8" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz#7bacdcbe71bdc3ff936d510c15dcea7cf0b99016" + integrity sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA== + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" @@ -1546,6 +1699,16 @@ "@babel/helper-simple-access" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-commonjs@^7.4.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz#8201101240eabb5a76c08ef61b2954f767b6b4c1" + integrity sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA== + dependencies: + "@babel/helper-module-transforms" "^7.15.4" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-simple-access" "^7.15.4" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-systemjs@^7.13.8": version "7.13.8" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" @@ -2096,6 +2259,15 @@ "@babel/parser" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/template@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194" + integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/traverse@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz#689f0e4b4c08587ad26622832632735fb8c4e0c0" @@ -2140,6 +2312,21 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.15.4": + version "7.15.4" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" + integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.15.4" + "@babel/helper-function-name" "^7.15.4" + "@babel/helper-hoist-variables" "^7.15.4" + "@babel/helper-split-export-declaration" "^7.15.4" + "@babel/parser" "^7.15.4" + "@babel/types" "^7.15.4" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz#8be1aa8f2c876da11a9cf650c0ecf656913ad611" @@ -2165,6 +2352,14 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.15.4", "@babel/types@^7.15.6": + version "7.15.6" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" + integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== + dependencies: + "@babel/helper-validator-identifier" "^7.14.9" + to-fast-properties "^2.0.0" + "@backstage/catalog-client@^0.3.18": version "0.3.19" resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.3.19.tgz#109b0fde1cc2d9a306635f3775fc8f6174172b14" @@ -2175,6 +2370,117 @@ "@backstage/errors" "^0.1.2" cross-fetch "^3.0.6" +"@backstage/cli@^0.7.11": + version "0.7.16" + resolved "https://roadiehq.jfrog.io/artifactory/api/npm/backstage-all/@backstage/cli/-/cli-0.7.16.tgz#42cf317b39ec7fe45617cc493f84482a91af44d0" + integrity sha1-Qs8xeznsf+RWF8xJP4RIKpGvRNA= + dependencies: + "@babel/core" "^7.4.4" + "@babel/plugin-transform-modules-commonjs" "^7.4.4" + "@backstage/cli-common" "^0.1.4" + "@backstage/config" "^0.1.10" + "@backstage/config-loader" "^0.6.10" + "@hot-loader/react-dom" "^16.13.0" + "@lerna/package-graph" "^4.0.0" + "@lerna/project" "^4.0.0" + "@octokit/request" "^5.4.12" + "@rollup/plugin-commonjs" "^17.1.0" + "@rollup/plugin-json" "^4.0.2" + "@rollup/plugin-node-resolve" "^13.0.0" + "@rollup/plugin-yaml" "^3.0.0" + "@spotify/eslint-config-base" "^9.0.0" + "@spotify/eslint-config-react" "^10.0.0" + "@spotify/eslint-config-typescript" "^10.0.0" + "@sucrase/jest-plugin" "^2.1.1" + "@sucrase/webpack-loader" "^2.0.0" + "@svgr/plugin-jsx" "5.5.x" + "@svgr/plugin-svgo" "5.4.x" + "@svgr/rollup" "5.5.x" + "@svgr/webpack" "5.5.x" + "@types/webpack-env" "^1.15.2" + "@typescript-eslint/eslint-plugin" "^v4.30.0" + "@typescript-eslint/parser" "^v4.28.3" + "@yarnpkg/lockfile" "^1.1.0" + babel-plugin-dynamic-import-node "^2.3.3" + bfj "^7.0.2" + buffer "^6.0.3" + chalk "^4.0.0" + chokidar "^3.3.1" + commander "^6.1.0" + css-loader "^5.2.6" + dashify "^2.0.0" + diff "^5.0.0" + esbuild "^0.8.56" + eslint "^7.30.0" + eslint-config-prettier "^8.3.0" + eslint-formatter-friendly "^7.0.0" + eslint-plugin-import "^2.20.2" + eslint-plugin-jest "^24.1.0" + eslint-plugin-jsx-a11y "^6.2.1" + eslint-plugin-monorepo "^0.3.2" + eslint-plugin-react "^7.12.4" + eslint-plugin-react-hooks "^4.0.0" + express "^4.17.1" + fork-ts-checker-webpack-plugin "^4.0.5" + fs-extra "9.1.0" + glob "^7.1.7" + handlebars "^4.7.3" + html-webpack-plugin "^5.3.1" + inquirer "^7.0.4" + jest "^26.0.1" + jest-css-modules "^2.1.0" + jest-transform-yaml "^0.1.1" + json-schema "^0.3.0" + lodash "^4.17.21" + mini-css-extract-plugin "^2.4.2" + node-libs-browser "^2.2.1" + ora "^5.3.0" + postcss "^8.1.0" + process "^0.11.10" + react "^16.0.0" + react-dev-utils "^11.0.4" + react-hot-loader "^4.12.21" + recursive-readdir "^2.2.2" + replace-in-file "^6.0.0" + rollup "2.44.x" + rollup-plugin-dts "^3.0.1" + rollup-plugin-esbuild "2.6.x" + rollup-plugin-peer-deps-external "^2.2.2" + rollup-plugin-postcss "^4.0.0" + rollup-pluginutils "^2.8.2" + run-script-webpack-plugin "^0.0.11" + semver "^7.3.2" + style-loader "^1.2.1" + sucrase "^3.20.2" + tar "^6.1.2" + terser-webpack-plugin "^5.1.3" + ts-loader "^8.0.17" + typescript "^4.0.3" + util "^0.12.3" + webpack "^5.48.0" + webpack-dev-server "4.0.0-rc.0" + webpack-node-externals "^3.0.0" + yaml "^1.10.0" + yml-loader "^2.1.0" + yn "^4.0.0" + +"@backstage/config-loader@^0.6.10": + version "0.6.10" + resolved "https://roadiehq.jfrog.io/artifactory/api/npm/backstage-all/@backstage/config-loader/-/config-loader-0.6.10.tgz#8999ffbe4a628297ff72fdccd510d72b479a8e26" + integrity sha1-iZn/vkpigpf/cv3M1RDXK0eajiY= + dependencies: + "@backstage/cli-common" "^0.1.4" + "@backstage/config" "^0.1.9" + "@types/json-schema" "^7.0.6" + ajv "^7.0.3" + chokidar "^3.5.2" + fs-extra "9.1.0" + json-schema "^0.3.0" + json-schema-merge-allof "^0.8.1" + typescript-json-schema "^0.50.1" + yaml "^1.9.2" + yup "^0.32.9" + "@backstage/core-api@^0.2.23": version "0.2.23" resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.23.tgz#c0ec13407ff7c78d376eb18e9d8e1490d54d995b" @@ -20351,7 +20657,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" From 7714547af59367d96f96a5807d38bb65fae6207d Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Thu, 21 Oct 2021 10:27:19 +0100 Subject: [PATCH 080/245] Fixing types Signed-off-by: Nicolas Arnold --- .changeset/lucky-books-worry.md | 2 +- plugins/auth-backend/api-report.md | 13 +++++-- .../src/providers/github/provider.test.ts | 8 ++-- .../src/providers/github/provider.ts | 38 +++++++++++++------ plugins/auth-backend/src/providers/types.ts | 5 ++- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/.changeset/lucky-books-worry.md b/.changeset/lucky-books-worry.md index f91dccb3f9..5645b4b4e9 100644 --- a/.changeset/lucky-books-worry.md +++ b/.changeset/lucky-books-worry.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': patch --- -Allow extra field in OAuth state parameter +Allow OAuth state to be encoded by a stateEncoder. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index d8275ef5d0..35c93e9537 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -293,7 +293,7 @@ export type GithubProviderOptions = { signIn?: { resolver?: SignInResolver; }; - stateHandler?: StateHandler; + stateEncoder?: StateEncoder; }; // Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -583,7 +583,12 @@ export type WebMessageResponse = // src/providers/atlassian/provider.d.ts:37:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/atlassian/provider.d.ts:42:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:65:5 - (ae-forgotten-export) The symbol "StateHandler" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:121:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative +// src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// src/providers/github/provider.d.ts:71:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/providers/github/provider.d.ts:71:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name +// src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:122:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index dd0910163f..44467ce66c 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -25,7 +25,7 @@ import { } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; -import { OAuthStartRequest, encodeState } from '../../lib/oauth'; +import { OAuthState, encodeState } from '../../lib/oauth'; const mockFrameHandler = jest.spyOn( helpers, @@ -57,9 +57,9 @@ describe('GithubAuthProvider', () => { authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), - stateHandler: async (req: OAuthStartRequest) => { - return encodeState(req.state); - }, + stateEncoder: async (state: OAuthState) => ({ + encodedState: encodeState(state), + }), callbackUrl: 'mock', clientId: 'mock', clientSecret: 'mock', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 63d2cf79c9..ff718dde87 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -31,7 +31,7 @@ import { AuthProviderFactory, AuthHandler, SignInResolver, - StateHandler, + StateEncoder, } from '../types'; import { OAuthAdapter, @@ -42,6 +42,7 @@ import { encodeState, OAuthRefreshRequest, OAuthResponse, + OAuthState, } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; @@ -67,7 +68,7 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { authorizationUrl?: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - stateHandler: StateHandler; + stateEncoder: StateEncoder; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; logger: Logger; @@ -80,12 +81,12 @@ export class GithubAuthProvider implements OAuthHandlers { private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; - private readonly stateHandler: StateHandler; + private readonly stateEncoder: StateEncoder; constructor(options: GithubAuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.stateHandler = options.stateHandler; + this.stateEncoder = options.stateEncoder; this.tokenIssuer = options.tokenIssuer; this.catalogIdentityClient = options.catalogIdentityClient; this.logger = options.logger; @@ -113,7 +114,7 @@ export class GithubAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, - state: await this.stateHandler(req.state), + state: await (await this.stateEncoder(req.state)).encodedState, }); } @@ -215,9 +216,22 @@ export type GithubProviderOptions = { }; /** - * The state handler that sets the uri query param 'state' + * The state encoder used to encode the 'state' parameter on the OAuth request. + * + * It should return a string that takes the state params (from the request), url encodes the params + * and finally base64 encodes them. + * + * Providing your own stateEncoder will allow you to add addition parameters to the state field. + * + * It is typed as follows: + * export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>; + * + * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail + * (These two values will be set by the req.state by default) + * + * For more information, please see the helper module in ../../oauth/helpers #readState */ - stateHandler?: StateHandler; + stateEncoder?: StateEncoder; }; export const createGithubProvider = ( @@ -272,10 +286,10 @@ export const createGithubProvider = ( logger, }); - const stateHandler: StateHandler = options?.stateHandler - ? options.stateHandler - : async (req: OAuthStartRequest) => { - return encodeState(req.state); + const stateEncoder: StateEncoder = options?.stateEncoder + ? options.stateEncoder + : async (state: OAuthState): Promise<{ encodedState: string }> => { + return { encodedState: encodeState(state) }; }; const provider = new GithubAuthProvider({ @@ -289,7 +303,7 @@ export const createGithubProvider = ( authHandler, tokenIssuer, catalogIdentityClient, - stateHandler, + stateEncoder, logger, }); diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 83e6e0bcb6..09e1935f3e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -21,6 +21,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity/types'; +import { OAuthState } from '../lib/oauth/types'; import { CatalogIdentityClient } from '../lib/catalog'; export type AuthProviderConfig = { @@ -224,4 +225,6 @@ export type AuthHandler = ( input: AuthResult, ) => Promise; -export type StateHandler = (input: any) => Promise; +export type StateEncoder = ( + input: OAuthState, +) => Promise<{ encodedState: string }>; From 33d99f7cdd963da3421b4570446641dd0dc890dd Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 22 Oct 2021 08:12:29 +0100 Subject: [PATCH 081/245] data store options type added Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 10 ++++---- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 23 +++++++++++++++---- .../tasks/StorageTaskBroker.test.ts | 4 +++- .../src/scaffolder/tasks/TaskWorker.test.ts | 4 +++- .../src/scaffolder/tasks/index.ts | 2 +- .../scaffolder-backend/src/service/router.ts | 6 ++--- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index f47c550875..deb2657c59 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -193,9 +193,7 @@ export const createTemplateAction: < templateAction: TemplateAction, ) => TemplateAction; -// Warning: (ae-missing-release-tag) "CreateWorkerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type CreateWorkerOptions = { taskBroker: TaskBroker; actionRegistry: TemplateActionRegistry; @@ -206,7 +204,7 @@ export type CreateWorkerOptions = { // @public export class DatabaseTaskStore implements TaskStore { - constructor(db: Knex); + constructor(options: DatabaseTaskStoreOptions); // (undocumented) claimTask(): Promise; // (undocumented) @@ -219,8 +217,10 @@ export class DatabaseTaskStore implements TaskStore { status: Status; eventBody: JsonObject; }): Promise; + // Warning: (ae-forgotten-export) The symbol "DatabaseTaskStoreOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - static create(knex: Knex): Promise; + static create(options: DatabaseTaskStoreOptions): Promise; // (undocumented) createTask( spec: TaskSpec, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 3aec10d949..6783da674e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -54,20 +54,35 @@ export type RawDbTaskEventRow = { created_at: string; }; +/** + * DatabaseTaskStore + * + * @public + */ +export type DatabaseTaskStoreOptions = { + database: Knex; +}; + /** * DatabaseTaskStore * * @public */ export class DatabaseTaskStore implements TaskStore { - static async create(knex: Knex): Promise { - await knex.migrate.latest({ + private readonly db: Knex; + + static async create( + options: DatabaseTaskStoreOptions, + ): Promise { + await options.database.migrate.latest({ directory: migrationsDir, }); - return new DatabaseTaskStore(knex); + return new DatabaseTaskStore(options); } - constructor(private readonly db: Knex) {} + constructor(options: DatabaseTaskStoreOptions) { + this.db = options.database; + } async getTask(taskId: string): Promise { const [result] = await this.db('tasks') diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 183253ea0e..c5d94c1324 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -31,7 +31,9 @@ async function createStore(): Promise { }, }), ).forPlugin('scaffolder'); - return await DatabaseTaskStore.create(await manager.getClient()); + return await DatabaseTaskStore.create({ + database: await manager.getClient(), + }); } describe('StorageTaskBroker', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 87a0229b5d..f6b332b6d3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -33,7 +33,9 @@ async function createStore(): Promise { }, }), ).forPlugin('scaffolder'); - return await DatabaseTaskStore.create(await manager.getClient()); + return await DatabaseTaskStore.create({ + database: await manager.getClient(), + }); } describe('TaskWorker', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index ada12df476..13831ca1bb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - export { DatabaseTaskStore } from './DatabaseTaskStore'; +export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; export { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; export type { TaskState } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index d4d4dd0486..0bc00a2778 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -92,9 +92,9 @@ export async function createRouter( const entityClient = new CatalogEntityClient(catalogClient); const integrations = ScmIntegrations.fromConfig(config); - const databaseTaskStore = await DatabaseTaskStore.create( - await database.getClient(), - ); + const databaseTaskStore = await DatabaseTaskStore.create({ + database: await database.getClient(), + }); const taskBroker = options.taskBroker || new StorageTaskBroker(databaseTaskStore, logger); const actionRegistry = new TemplateActionRegistry(); From 89950060ef0282bf70f9eec80a4f6e77c4c4012e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 22 Oct 2021 15:02:34 +0200 Subject: [PATCH 082/245] release @backstage/types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/config/package.json | 2 +- packages/core-plugin-api/package.json | 2 +- packages/types/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/config/package.json b/packages/config/package.json index da741b7a3b..88c20a76ea 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/types": "^0.1.0", + "@backstage/types": "^0.1.1", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index aeb35faa2f..38e3f4aebe 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -31,7 +31,7 @@ "dependencies": { "@backstage/config": "^0.1.9", "@backstage/theme": "^0.2.9", - "@backstage/types": "^0.1.0", + "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@types/react": "*", diff --git a/packages/types/package.json b/packages/types/package.json index 9da8cd5e95..8632422a25 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/types", "description": "Common TypeScript types used within Backstage", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 5df2435892d4961a844637bb611e5f9a8710e75d Mon Sep 17 00:00:00 2001 From: OscarDHdz Date: Fri, 22 Oct 2021 08:23:43 -0500 Subject: [PATCH 083/245] Scaffolder: Enable buttons if template fails to execute Signed-off-by: OscarDHdz --- .changeset/green-tips-eat.md | 5 +++++ .../components/MultistepJsonForm/MultistepJsonForm.tsx | 9 ++++++--- .../src/components/TemplatePage/TemplatePage.tsx | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .changeset/green-tips-eat.md diff --git a/.changeset/green-tips-eat.md b/.changeset/green-tips-eat.md new file mode 100644 index 0000000000..b8c631fbf4 --- /dev/null +++ b/.changeset/green-tips-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Scaffolder: Enable back Template action buttons if template fails to execute diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 62c2881ab0..f292d4e179 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -44,7 +44,7 @@ type Props = { formData: Record; onChange: (e: IChangeEvent) => void; onReset: () => void; - onFinish: () => void; + onFinish: () => Promise; widgets?: FormProps['widgets']; fields?: FormProps['fields']; }; @@ -122,9 +122,12 @@ export const MultistepJsonForm = ({ setActiveStep(Math.min(activeStep + 1, steps.length)); }; const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0)); - const handleCreate = () => { + const handleCreate = async () => { setDisableButtons(true); - onFinish(); + const success = await onFinish(); + if (!success) { + setDisableButtons(false); + } }; return ( diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 05533376db..7b6a217543 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -132,8 +132,10 @@ export const TemplatePage = ({ const id = await scaffolderApi.scaffold(templateName, formState); navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id })); + return true; } catch (e) { errorApi.post(e); + return false; } }; From b9602099765c3063cbcfef6374c5f6735033db98 Mon Sep 17 00:00:00 2001 From: Kenneth Feng Date: Fri, 22 Oct 2021 10:01:55 -0400 Subject: [PATCH 084/245] fix style issues Signed-off-by: Kenneth Feng --- .../actions/builtin/publish/githubPullRequest.test.ts | 6 +++--- .../actions/builtin/publish/githubPullRequest.ts | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index dbb5f3caff..1de9a7e268 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -172,9 +172,9 @@ describe('createPublishGithubPullRequestAction', () => { commit: 'Create my new app', files: { 'foo.txt': { - content: 'Hello there!', - encoding: 'utf-8', - mode: '100644', + content: 'Hello there!', + encoding: 'utf-8', + mode: '100644', }, }, }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 733fd673ff..aec59c86fd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -198,7 +198,7 @@ export const createPublishGithubPullRequestAction = ({ const absPath = path.resolve(fileRoot, filePath); const content = fs.readFileSync(absPath).toString(); const fileStat = fs.statSync(absPath); - const isExecutable = fileStat.mode === 33277 // aka. 100755; + const isExecutable = fileStat.mode === 33277; // aka. 100755 // See the properties of tree items // in https://docs.github.com/en/rest/reference/git#trees const githubTreeItemMode = isExecutable ? '100755' : '100644'; @@ -216,10 +216,7 @@ export const createPublishGithubPullRequestAction = ({ const changes = [ { - files: zipObject( - repoFilePaths, - fileContents, - ), + files: zipObject(repoFilePaths, fileContents), commit: title, }, ]; From 56f182dde2767d8e700d5ed0c16c052891f54626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 22 Oct 2021 15:52:51 +0200 Subject: [PATCH 085/245] Remove all internal usage of the Json types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/api-report.md | 2 +- packages/backend-common/package.json | 1 + packages/backend-common/src/cache/CacheClient.ts | 2 +- packages/backend-common/src/config.ts | 3 ++- packages/backend-common/src/database/DatabaseManager.ts | 3 ++- packages/backend-common/src/database/connection.ts | 3 ++- .../backend-common/src/reading/AwsS3UrlReader.test.ts | 4 +++- .../backend-common/src/reading/GoogleGcsUrlReader.test.ts | 3 ++- packages/catalog-model/api-report.md | 4 ++-- packages/catalog-model/package.json | 1 + packages/catalog-model/src/entity/Entity.ts | 2 +- packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts | 2 +- packages/catalog-model/src/types.ts | 2 +- packages/cli/package.json | 1 + packages/cli/src/commands/config/docs.ts | 2 +- packages/cli/src/commands/config/schema.ts | 2 +- packages/cli/src/commands/install/types.ts | 2 +- packages/config-loader/api-report.md | 2 +- packages/config-loader/package.json | 1 + packages/config-loader/src/lib/env.ts | 3 ++- packages/config-loader/src/lib/schema/collect.ts | 2 +- packages/config-loader/src/lib/schema/filtering.test.ts | 2 +- packages/config-loader/src/lib/schema/filtering.ts | 2 +- packages/config-loader/src/lib/schema/load.ts | 3 ++- packages/config-loader/src/lib/schema/types.ts | 3 ++- packages/config-loader/src/lib/transform/apply.ts | 2 +- packages/config-loader/src/lib/transform/include.ts | 2 +- packages/config-loader/src/lib/transform/substitution.ts | 2 +- packages/config-loader/src/lib/transform/types.ts | 2 +- packages/config-loader/src/lib/transform/utils.ts | 2 +- packages/core-app-api/package.json | 1 + packages/core-app-api/src/app/createApp.tsx | 3 ++- packages/errors/api-report.md | 2 +- packages/errors/package.json | 2 +- packages/errors/src/serialization/error.ts | 2 +- packages/search-common/api-report.md | 2 +- packages/search-common/package.json | 2 +- packages/search-common/src/types.ts | 2 +- plugins/app-backend/package.json | 2 ++ plugins/app-backend/src/lib/config.ts | 3 ++- plugins/catalog-backend-module-ldap/api-report.md | 2 +- plugins/catalog-backend-module-ldap/package.json | 1 + plugins/catalog-backend-module-ldap/src/ldap/config.ts | 3 ++- plugins/catalog-backend/api-report.md | 4 ++-- plugins/catalog-backend/package.json | 1 + plugins/catalog-backend/src/database/types.ts | 2 +- .../src/ingestion/processors/PlaceholderProcessor.ts | 2 +- plugins/catalog-backend/src/ingestion/processors/types.ts | 2 +- .../processing/DefaultCatalogProcessingOrchestrator.ts | 2 +- .../src/processing/ProcessorCacheManager.ts | 2 +- plugins/catalog-backend/src/processing/types.ts | 2 +- plugins/catalog-backend/src/processing/util.ts | 2 +- plugins/catalog-graphql/package.json | 1 + plugins/catalog-graphql/src/service/client.ts | 2 +- plugins/config-schema/package.json | 1 + .../config-schema/src/components/SchemaView/ChildView.tsx | 2 +- .../src/components/SchemaView/MetadataView.tsx | 2 +- .../scaffolder-backend-module-cookiecutter/package.json | 1 + .../src/actions/fetch/cookiecutter.test.ts | 3 ++- .../src/actions/fetch/cookiecutter.ts | 2 +- plugins/scaffolder-backend-module-rails/package.json | 1 + .../src/actions/fetch/rails/index.ts | 2 +- .../src/actions/fetch/rails/railsNewRunner.ts | 2 +- plugins/scaffolder-backend-module-yeoman/package.json | 1 + .../src/actions/run/yeoman.test.ts | 2 +- .../src/actions/run/yeoman.ts | 2 +- .../src/actions/run/yeomanRun.ts | 2 +- plugins/scaffolder-backend/api-report.md | 8 +++----- plugins/scaffolder-backend/package.json | 1 + .../src/scaffolder/actions/builtin/fetch/helpers.ts | 2 +- .../src/scaffolder/actions/builtin/filesystem/rename.ts | 2 +- .../scaffolder-backend/src/scaffolder/actions/types.ts | 2 +- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 2 +- .../src/scaffolder/tasks/DefaultWorkflowRunner.ts | 2 +- .../src/scaffolder/tasks/LegacyWorkflowRunner.ts | 2 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 2 +- plugins/scaffolder-backend/src/scaffolder/tasks/types.ts | 2 +- plugins/scaffolder-common/api-report.md | 2 +- plugins/scaffolder-common/package.json | 2 +- plugins/scaffolder-common/src/TemplateEntityV1beta3.ts | 2 +- plugins/scaffolder/api-report.md | 4 ++-- plugins/scaffolder/package.json | 1 + plugins/scaffolder/src/api.ts | 2 +- .../components/MultistepJsonForm/MultistepJsonForm.tsx | 2 +- .../scaffolder/src/components/MultistepJsonForm/schema.ts | 2 +- .../src/components/TemplatePage/TemplatePage.tsx | 2 +- plugins/scaffolder/src/types.ts | 2 +- plugins/search/api-report.md | 2 +- plugins/search/package.json | 1 + .../search/src/components/SearchContext/SearchContext.tsx | 2 +- plugins/search/src/components/SearchPage/SearchPage.tsx | 2 +- 91 files changed, 110 insertions(+), 82 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index e2d68055bf..2336a57ec3 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -18,7 +18,7 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; import { isChildPath } from '@backstage/cli-common'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { Logger as Logger_2 } from 'winston'; import { MergeResult } from 'isomorphic-git'; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 8da4184f71..60824f0c40 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -34,6 +34,7 @@ "@backstage/config-loader": "^0.7.0", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.7", + "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 79f48a2757..8e046f7f19 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { createHash } from 'crypto'; import Keyv from 'keyv'; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 04cf1ac2bb..fa49d5f015 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,7 +18,8 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader, JsonValue } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { loadConfig } from '@backstage/config-loader'; export class ObservableConfigProxy implements Config { diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index de39279a4f..c0511c6b78 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -15,7 +15,8 @@ */ import { Knex } from 'knex'; import { omit } from 'lodash'; -import { Config, ConfigReader, JsonObject } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { createDatabaseClient, ensureDatabaseExists, diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 8ea46070eb..1c5cce3718 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Config, JsonObject } from '@backstage/config'; +import { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index 6959edcc8b..301543e8c9 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConfigReader, JsonObject } from '@backstage/config'; + +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { AwsS3UrlReader } from './AwsS3UrlReader'; diff --git a/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts index d8aac4886d..31cf2956f8 100644 --- a/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { ConfigReader, JsonObject } from '@backstage/config'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index c9429f9bdb..52f36493b5 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -3,9 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; import * as yup from 'yup'; diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 69d1c34ba9..615e80b684 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -32,6 +32,7 @@ "dependencies": { "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.13", "ajv": "^7.0.3", diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 7a4729c309..9d65d3e8f3 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { EntityName } from '../types'; import { UNSTABLE_EntityStatus } from './EntityStatus'; diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index 1b1966d05d..f4c2dfb5bc 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Template.v1beta2.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 8ec1303a40..1a0fab267c 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; /** diff --git a/packages/cli/package.json b/packages/cli/package.json index da9e161fef..62a008087e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,6 +32,7 @@ "@backstage/config": "^0.1.10", "@backstage/config-loader": "^0.7.0", "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", diff --git a/packages/cli/src/commands/config/docs.ts b/packages/cli/src/commands/config/docs.ts index 198244b941..7a807f7c78 100644 --- a/packages/cli/src/commands/config/docs.ts +++ b/packages/cli/src/commands/config/docs.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; import { Command } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; diff --git a/packages/cli/src/commands/config/schema.ts b/packages/cli/src/commands/config/schema.ts index a36fc24f06..be228e1e7c 100644 --- a/packages/cli/src/commands/config/schema.ts +++ b/packages/cli/src/commands/config/schema.ts @@ -18,7 +18,7 @@ import { Command } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../../lib/config'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; export default async (cmd: Command) => { diff --git a/packages/cli/src/commands/install/types.ts b/packages/cli/src/commands/install/types.ts index 5479032de5..3105f78822 100644 --- a/packages/cli/src/commands/install/types.ts +++ b/packages/cli/src/commands/install/types.ts @@ -15,7 +15,7 @@ */ import { YarnInfoInspectData } from '../../lib/versioning'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /** * TODO: possible types diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index b47119d470..19b5ee9e27 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -4,7 +4,7 @@ ```ts import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; // @public diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 03ea61abe2..a731225e2b 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -33,6 +33,7 @@ "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.9", "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "chokidar": "^3.5.2", diff --git a/packages/config-loader/src/lib/env.ts b/packages/config-loader/src/lib/env.ts index 57cfc077a7..638b49dbc8 100644 --- a/packages/config-loader/src/lib/env.ts +++ b/packages/config-loader/src/lib/env.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { assertError } from '@backstage/errors'; const ENV_PREFIX = 'APP_CONFIG_'; diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index de6aff376f..8e7f582e44 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -23,7 +23,7 @@ import { } from 'path'; import { ConfigSchemaPackageEntry } from './types'; import { getProgramFromFiles, generateSchema } from 'typescript-json-schema'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { assertError } from '@backstage/errors'; type Item = { diff --git a/packages/config-loader/src/lib/schema/filtering.test.ts b/packages/config-loader/src/lib/schema/filtering.test.ts index 3b9e548026..0395c7da6a 100644 --- a/packages/config-loader/src/lib/schema/filtering.test.ts +++ b/packages/config-loader/src/lib/schema/filtering.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { ConfigVisibility } from './types'; import { filterByVisibility, filterErrorsByVisibility } from './filtering'; diff --git a/packages/config-loader/src/lib/schema/filtering.ts b/packages/config-loader/src/lib/schema/filtering.ts index 2b13f7ac3f..b4226d7dad 100644 --- a/packages/config-loader/src/lib/schema/filtering.ts +++ b/packages/config-loader/src/lib/schema/filtering.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { ConfigVisibility, DEFAULT_CONFIG_VISIBILITY, diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index f3ea0b900e..2d78785304 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { compileConfigSchemas } from './compile'; import { collectConfigSchemas } from './collect'; import { filterByVisibility, filterErrorsByVisibility } from './filtering'; diff --git a/packages/config-loader/src/lib/schema/types.ts b/packages/config-loader/src/lib/schema/types.ts index d1c07f4dac..53f4dec88e 100644 --- a/packages/config-loader/src/lib/schema/types.ts +++ b/packages/config-loader/src/lib/schema/types.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /** * An sub-set of configuration schema. diff --git a/packages/config-loader/src/lib/transform/apply.ts b/packages/config-loader/src/lib/transform/apply.ts index 091f9c23c5..80ddad90d0 100644 --- a/packages/config-loader/src/lib/transform/apply.ts +++ b/packages/config-loader/src/lib/transform/apply.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { assertError } from '@backstage/errors'; import { TransformFunc } from './types'; import { isObject } from './utils'; diff --git a/packages/config-loader/src/lib/transform/include.ts b/packages/config-loader/src/lib/transform/include.ts index 5ea7161e5f..569b61c82c 100644 --- a/packages/config-loader/src/lib/transform/include.ts +++ b/packages/config-loader/src/lib/transform/include.ts @@ -16,7 +16,7 @@ import yaml from 'yaml'; import { extname, dirname, resolve as resolvePath } from 'path'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { isObject } from './utils'; import { TransformFunc, EnvFunc, ReadFileFunc } from './types'; diff --git a/packages/config-loader/src/lib/transform/substitution.ts b/packages/config-loader/src/lib/transform/substitution.ts index 21edfb7e57..3c25784dc2 100644 --- a/packages/config-loader/src/lib/transform/substitution.ts +++ b/packages/config-loader/src/lib/transform/substitution.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { TransformFunc, EnvFunc } from './types'; /** diff --git a/packages/config-loader/src/lib/transform/types.ts b/packages/config-loader/src/lib/transform/types.ts index 20e5f88718..0b743cb0fe 100644 --- a/packages/config-loader/src/lib/transform/types.ts +++ b/packages/config-loader/src/lib/transform/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; export type EnvFunc = (name: string) => Promise; diff --git a/packages/config-loader/src/lib/transform/utils.ts b/packages/config-loader/src/lib/transform/utils.ts index a49e9b14bd..b28effa023 100644 --- a/packages/config-loader/src/lib/transform/utils.ts +++ b/packages/config-loader/src/lib/transform/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue, JsonObject } from '@backstage/config'; +import { JsonValue, JsonObject } from '@backstage/types'; export function isObject(obj: JsonValue | undefined): obj is JsonObject { if (typeof obj !== 'object') { diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index d11b7122ce..1eca9249fc 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -33,6 +33,7 @@ "@backstage/config": "^0.1.10", "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index e9c37a1420..901697edba 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { Button } from '@material-ui/core'; import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components'; import { darkTheme, lightTheme } from '@backstage/theme'; diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index ffb111884e..a96132cee7 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; // @public export function assertError(value: unknown): asserts value is ErrorLike; diff --git a/packages/errors/package.json b/packages/errors/package.json index e3d49f55c7..b1f214e5af 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.9", + "@backstage/types": "^0.1.1", "cross-fetch": "^3.0.6", "serialize-error": "^8.0.1" }, diff --git a/packages/errors/src/serialization/error.ts b/packages/errors/src/serialization/error.ts index b1f63063cf..8d031949bf 100644 --- a/packages/errors/src/serialization/error.ts +++ b/packages/errors/src/serialization/error.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { deserializeError as deserializeErrorInternal, serializeError as serializeErrorInternal, diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index d8d9c2922a..c4950e3556 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; // Warning: (ae-missing-release-tag) "DocumentCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/search-common/package.json b/packages/search-common/package.json index 46e3518b51..fee0fe59dd 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -36,7 +36,7 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/config": "^0.1.6" + "@backstage/types": "^0.1.1" }, "devDependencies": { "@backstage/cli": "^0.8.0" diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 063e0cb96c..f73665de5b 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; export interface SearchQuery { term: string; diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 6047b5aeb1..3483bfa8bd 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -33,6 +33,7 @@ "@backstage/backend-common": "^0.9.7", "@backstage/config-loader": "^0.7.0", "@backstage/config": "^0.1.8", + "@backstage/types": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -42,6 +43,7 @@ }, "devDependencies": { "@backstage/cli": "^0.8.0", + "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/app-backend/src/lib/config.ts b/plugins/app-backend/src/lib/config.ts index 1ebd95a5c0..c37dc34dcc 100644 --- a/plugins/app-backend/src/lib/config.ts +++ b/plugins/app-backend/src/lib/config.ts @@ -17,7 +17,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { Logger } from 'winston'; -import { AppConfig, Config, JsonObject } from '@backstage/config'; +import { AppConfig, Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { loadConfigSchema, readEnvConfig } from '@backstage/config-loader'; type InjectOptions = { diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index ae4a085649..bbf92944b9 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -10,7 +10,7 @@ import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { SearchEntry } from 'ldapjs'; diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 7236e780c4..51c8780a9d 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -33,6 +33,7 @@ "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/types": "^0.1.1", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 6dad7e087f..dc1ec4910d 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Config, JsonValue } from '@backstage/config'; +import { Config } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { SearchOptions } from 'ldapjs'; import mergeWith from 'lodash/mergeWith'; import { RecursivePartial } from '@backstage/plugin-catalog-backend'; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index d3c73afc91..cbef4c90a1 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -18,8 +18,8 @@ import { EntityRelationSpec } from '@backstage/catalog-model'; import express from 'express'; import { GitHubIntegrationConfig } from '@backstage/integration'; import { IndexableDocument } from '@backstage/search-common'; -import { JsonObject } from '@backstage/config'; -import { JsonValue } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { Location as Location_2 } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 896bbde592..9a44258958 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -37,6 +37,7 @@ "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/search-common": "^0.2.0", + "@backstage/types": "^0.1.1", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 9d28387e2c..ac2ff07498 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -15,7 +15,7 @@ */ import { Entity, EntityRelationSpec } from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { DateTime } from 'luxon'; import { DeferredEntity } from '../processing/types'; diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts index 9ddef1ab3b..65f65a0479 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts @@ -16,7 +16,7 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity, LocationSpec } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import yaml from 'yaml'; import { CatalogProcessor } from './types'; diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index dccc60d42c..3e4e24b0fe 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -19,7 +19,7 @@ import { EntityRelationSpec, LocationSpec, } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; export type CatalogProcessor = { /** diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index d3ec162216..b42c8839cf 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -29,7 +29,7 @@ import { InputError, NotAllowedError, } from '@backstage/errors'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; import { Logger } from 'winston'; diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts index 378974e33e..472d6453ce 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { CatalogProcessor } from '../ingestion/processors'; import { CatalogProcessorCache } from '../ingestion/processors/types'; import { isObject } from './util'; diff --git a/plugins/catalog-backend/src/processing/types.ts b/plugins/catalog-backend/src/processing/types.ts index 4b16ccef32..7fb1157d3b 100644 --- a/plugins/catalog-backend/src/processing/types.ts +++ b/plugins/catalog-backend/src/processing/types.ts @@ -15,7 +15,7 @@ */ import { Entity, EntityRelationSpec } from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; export type EntityProcessingRequest = { entity: Entity; diff --git a/plugins/catalog-backend/src/processing/util.ts b/plugins/catalog-backend/src/processing/util.ts index ad8fe8be60..9f5f23ed09 100644 --- a/plugins/catalog-backend/src/processing/util.ts +++ b/plugins/catalog-backend/src/processing/util.ts @@ -24,7 +24,7 @@ import { ORIGIN_LOCATION_ANNOTATION, stringifyEntityRef, } from '@backstage/catalog-model'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 80ad7f9a16..f5705dcc76 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -33,6 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.8", + "@backstage/types": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", "cross-fetch": "^3.0.6", diff --git a/plugins/catalog-graphql/src/service/client.ts b/plugins/catalog-graphql/src/service/client.ts index 487762d5d7..4328a5bd5a 100644 --- a/plugins/catalog-graphql/src/service/client.ts +++ b/plugins/catalog-graphql/src/service/client.ts @@ -16,7 +16,7 @@ import { Entity, EntityMeta } from '@backstage/catalog-model'; import fetch from 'cross-fetch'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; export interface ReaderEntityMeta extends EntityMeta { uid: string; diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 1b2214de8b..1a6b39f0cb 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -26,6 +26,7 @@ "@backstage/core-plugin-api": "^0.1.11", "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/plugins/config-schema/src/components/SchemaView/ChildView.tsx b/plugins/config-schema/src/components/SchemaView/ChildView.tsx index 03cb1201a0..dcb446db5a 100644 --- a/plugins/config-schema/src/components/SchemaView/ChildView.tsx +++ b/plugins/config-schema/src/components/SchemaView/ChildView.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { Box, Chip, Divider, makeStyles, Typography } from '@material-ui/core'; import { Schema } from 'jsonschema'; import React, { useEffect, useRef } from 'react'; diff --git a/plugins/config-schema/src/components/SchemaView/MetadataView.tsx b/plugins/config-schema/src/components/SchemaView/MetadataView.tsx index c7113950cc..157890ae6a 100644 --- a/plugins/config-schema/src/components/SchemaView/MetadataView.tsx +++ b/plugins/config-schema/src/components/SchemaView/MetadataView.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { Paper, Table, diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 7db57dbe6b..1f004b8804 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -25,6 +25,7 @@ "@backstage/integration": "^0.6.2", "@backstage/plugin-scaffolder-backend": "^0.15.10", "@backstage/config": "^0.1.8", + "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "10.0.0", "winston": "^3.2.1", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index bc3fe3eca2..bba51e3f01 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -29,7 +29,8 @@ import { UrlReader, ContainerRunner, } from '@backstage/backend-common'; -import { ConfigReader, JsonObject } from '@backstage/config'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import mockFs from 'mock-fs'; import os from 'os'; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 1f6fbdb7d8..5120cd3b27 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -19,7 +19,7 @@ import { UrlReader, resolveSafeChildPath, } from '@backstage/backend-common'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import commandExists from 'command-exists'; diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index c88e6d0f9f..5e1c9d0107 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -26,6 +26,7 @@ "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.2", + "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index e2dc8fd95a..5ce7d46689 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -15,7 +15,7 @@ */ import { ContainerRunner, UrlReader } from '@backstage/backend-common'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts index a828800e84..27ce32dde2 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsNewRunner.ts @@ -23,7 +23,7 @@ import { railsArgumentResolver, RailsRunOptions, } from './railsArgumentResolver'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { Writable } from 'stream'; export class RailsNewRunner { diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index eb339b90d5..a1f1635808 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/config": "^0.1.8", "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/types": "^0.1.1", "winston": "^3.2.1", "yeoman-environment": "^3.6.0" }, diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts index 6e61ae77c4..ee5e8449f7 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts @@ -23,7 +23,7 @@ import os from 'os'; import { PassThrough } from 'stream'; import { createRunYeomanAction } from './yeoman'; import type { ActionContext } from '@backstage/plugin-scaffolder-backend'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; describe('run:yeoman', () => { const mockTmpDir = os.tmpdir(); diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts index c8bd49f3d3..f6a29037a3 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; import { yeomanRun } from './yeomanRun'; diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeomanRun.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeomanRun.ts index ff11634f81..a18ea38c46 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeomanRun.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeomanRun.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /* * This module should use '@types/yeoman-environment' eventually as soon as '@types/yeoman-environment' supports diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 07c6c7a57d..1a7aebf2f5 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -14,10 +14,8 @@ import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-back import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { JsonObject } from '@backstage/config'; -import { JsonObject as JsonObject_2 } from '@backstage/types'; -import { JsonValue } from '@backstage/config'; -import { JsonValue as JsonValue_2 } from '@backstage/types'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Octokit } from '@octokit/rest'; @@ -185,7 +183,7 @@ export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export const createTemplateAction: < Input extends Partial<{ - [name: string]: JsonValue_2 | Partial | undefined; + [name: string]: JsonValue | Partial | undefined; }>, >( templateAction: TemplateAction, diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ea29866485..cfea66284d 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -39,6 +39,7 @@ "@backstage/plugin-catalog-backend": "^0.17.1", "@backstage/plugin-scaffolder-common": "^0.1.0", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", + "@backstage/types": "^0.1.1", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts index 5625989417..3946110d42 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts @@ -15,7 +15,7 @@ */ import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts index aaadd5df57..4bdba96750 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -17,7 +17,7 @@ import { createTemplateAction } from '../../createTemplateAction'; import { resolveSafeChildPath } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import fs from 'fs-extra'; interface FilesToRename extends JsonObject { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 3783359b95..ca3459d29c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -16,7 +16,7 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; -import { JsonValue, JsonObject } from '@backstage/config'; +import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; type PartialJsonObject = Partial; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 09cba596b1..9c72a310c1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { resolvePackagePath } from '@backstage/backend-common'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts index 2ac309a1d4..6c68e0c4c5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts @@ -26,7 +26,7 @@ import * as winston from 'winston'; import nunjucks from 'nunjucks'; import fs from 'fs-extra'; import path from 'path'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { isTruthy } from './helper'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts index 97e3a4dec4..6d43ae978c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts @@ -31,7 +31,7 @@ import { Logger } from 'winston'; import path from 'path'; import fs from 'fs-extra'; import { validate as validateJsonSchema } from 'jsonschema'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; type Options = { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 4cd1066fc9..2a99c3eb30 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; import { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index cd93404165..b91930c56a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue, JsonObject } from '@backstage/config'; +import { JsonValue, JsonObject } from '@backstage/types'; export type Status = | 'open' diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 722e746967..8485e224b2 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -4,7 +4,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { JSONSchema } from '@backstage/catalog-model'; // @public (undocumented) diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index b0c8de30c7..3c1c444a78 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10" + "@backstage/types": "^0.1.1" }, "devDependencies": { "@backstage/cli": "^0.8.0" diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts index 6471fe0ca6..2aefa5fd6b 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { Entity } from '@backstage/catalog-model'; /** @public */ diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 949f19a058..74abada9a8 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -19,9 +19,9 @@ import { FieldProps } from '@rjsf/core'; import { FieldValidation } from '@rjsf/core'; import { IconButton } from '@material-ui/core'; import { IdentityApi } from '@backstage/core-plugin-api'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { JSONSchema } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 10483f4807..aa4412204a 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -41,6 +41,7 @@ "@backstage/integration-react": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 3e0ac3eccb..1511899c65 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -15,7 +15,7 @@ */ import { EntityName } from '@backstage/catalog-model'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { Field, FieldValidation } from '@rjsf/core'; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 62c2881ab0..df5be2de56 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { Box, Button, diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts index dd86784275..39592d314d 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { FormProps } from '@rjsf/core'; function isObject(value: unknown): value is JsonObject { diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 05533376db..0ea53212c2 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { LinearProgress } from '@material-ui/core'; import { FormValidation, IChangeEvent } from '@rjsf/core'; import React, { useCallback, useState } from 'react'; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index dc71d44f71..5597082f22 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JSONSchema } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped'; export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index b29149805c..c638fb4af6 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -9,7 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/search-common'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; diff --git a/plugins/search/package.json b/plugins/search/package.json index 9830d26e5c..ee8a2fb726 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -38,6 +38,7 @@ "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index 16d1371964..11d6a2786e 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { useApi } from '@backstage/core-plugin-api'; import { SearchResultSet } from '@backstage/search-common'; import React, { diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index fb18833da6..a0738b3466 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -19,7 +19,7 @@ import { usePrevious } from 'react-use'; import qs from 'qs'; import { useLocation, useOutlet } from 'react-router'; import { SearchContextProvider, useSearch } from '../SearchContext'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { LegacySearchPage } from '../LegacySearchPage'; export const UrlUpdater = () => { From 10615525f364e7b91820334e5787226c9f5a9283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 22 Oct 2021 16:11:23 +0200 Subject: [PATCH 086/245] Do Observable too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/popular-apples-sparkle.md | 26 +++++++++++++++++++ packages/core-app-api/api-report.md | 5 ++-- .../AlertApi/AlertApiForwarder.ts | 4 ++- .../AppThemeApi/AppThemeSelector.ts | 3 ++- .../ErrorApi/ErrorApiForwarder.ts | 4 ++- .../OAuthRequestApi/OAuthPendingRequests.ts | 2 +- .../OAuthRequestApi/OAuthRequestManager.ts | 2 +- .../implementations/StorageApi/WebStorage.ts | 3 ++- .../implementations/auth/github/GithubAuth.ts | 2 +- .../implementations/auth/oauth2/OAuth2.ts | 2 +- .../implementations/auth/saml/SamlAuth.ts | 2 +- .../OptionalRefreshSessionManagerMux.test.ts | 3 ++- .../OptionalRefreshSessionManagerMux.ts | 3 ++- .../AuthSessionManager/SessionStateTracker.ts | 3 ++- .../src/lib/AuthSessionManager/types.ts | 3 ++- packages/core-app-api/src/lib/subjects.ts | 2 +- packages/test-utils/api-report.md | 2 +- packages/test-utils/package.json | 1 + .../testUtils/apis/ErrorApi/MockErrorApi.ts | 3 ++- .../apis/StorageApi/MockStorageApi.ts | 7 ++--- plugins/catalog-react/api-report.md | 2 +- plugins/catalog-react/package.json | 1 + .../DefaultStarredEntitiesApi.ts | 3 ++- .../StarredEntitiesApi/StarredEntitiesApi.ts | 3 ++- plugins/config-schema/api-report.md | 2 +- .../src/api/StaticSchemaLoader.ts | 2 +- plugins/config-schema/src/api/types.ts | 3 ++- plugins/scaffolder/api-report.md | 2 +- plugins/scaffolder/src/api.ts | 3 +-- .../src/components/hooks/useEventStream.ts | 3 ++- plugins/shortcuts/api-report.md | 2 +- plugins/shortcuts/package.json | 1 + plugins/shortcuts/src/api/ShortcutApi.ts | 3 ++- 33 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 .changeset/popular-apples-sparkle.md diff --git a/.changeset/popular-apples-sparkle.md b/.changeset/popular-apples-sparkle.md new file mode 100644 index 0000000000..b8437795da --- /dev/null +++ b/.changeset/popular-apples-sparkle.md @@ -0,0 +1,26 @@ +--- +'@backstage/backend-common': patch +'@backstage/catalog-model': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/core-app-api': patch +'@backstage/errors': patch +'@backstage/search-common': patch +'@backstage/test-utils': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-graphql': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-search': patch +'@backstage/plugin-shortcuts': patch +--- + +Switch to use the json and observable types from `@backstage/types` diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 594a0825a7..03c29daa01 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -40,8 +40,7 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/core-plugin-api'; -import { Observable as Observable_2 } from '@backstage/types'; +import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; @@ -341,7 +340,7 @@ export const defaultConfigLoader: AppConfigLoader; export class ErrorAlerter implements ErrorApi { constructor(alertApi: AlertApi, errorApi: ErrorApi); // (undocumented) - error$(): Observable_2<{ + error$(): Observable<{ error: { name: string; message: string; diff --git a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts index 0ba275ea4d..5e649e2f4c 100644 --- a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AlertApi, AlertMessage, Observable } from '@backstage/core-plugin-api'; + +import { AlertApi, AlertMessage } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; /** diff --git a/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts b/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts index 4dceed4749..3dfa43b661 100644 --- a/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts +++ b/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppThemeApi, AppTheme, Observable } from '@backstage/core-plugin-api'; +import { AppThemeApi, AppTheme } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { BehaviorSubject } from '../../../lib/subjects'; const STORAGE_KEY = 'theme'; diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts index 9c9a6f20f1..0edbd2f353 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, Observable } from '@backstage/core-plugin-api'; + +import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; /** diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts index d91cb0ebb0..be643d274b 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { BehaviorSubject } from '../../../lib/subjects'; type RequestQueueEntry = { diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts index 6c00cd85cd..1cd25c4b56 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts @@ -19,8 +19,8 @@ import { PendingAuthRequest, AuthRequester, AuthRequesterOptions, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests'; import { BehaviorSubject } from '../../../lib/subjects'; diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index d1f712e72a..08c4df7e8c 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { StorageApi, StorageValueChange, ErrorApi, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; const buckets = new Map(); diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 85e3e79ad5..72b4209a34 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -24,8 +24,8 @@ import { ProfileInfo, BackstageIdentity, AuthRequestOptions, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { AuthSessionStore, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 24db8d2560..8f685b9266 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -28,8 +28,8 @@ import { SessionState, SessionApi, BackstageIdentityApi, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { OAuth2Session } from './types'; import { OAuthApiCreateOptions } from '../types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index ae4f80c9b1..0a7fb50101 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -25,8 +25,8 @@ import { ProfileInfoApi, BackstageIdentityApi, SessionApi, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { SamlSession } from './types'; import { AuthSessionStore, diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts index 919948f531..35b9a808b2 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { OptionalRefreshSessionManagerMux } from './OptionalRefreshSessionManagerMux'; import { MutableSessionManager, SessionManager } from './types'; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts index 221a7635ea..80dfa1879e 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { SessionManager, MutableSessionManager, diff --git a/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts b/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts index b70b4bf4ca..4523a868b4 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts @@ -15,7 +15,8 @@ */ import { BehaviorSubject } from '../subjects'; -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; export class SessionStateTracker { private readonly subject = new BehaviorSubject( diff --git a/packages/core-app-api/src/lib/AuthSessionManager/types.ts b/packages/core-app-api/src/lib/AuthSessionManager/types.ts index 43655b8395..a2a5800974 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/types.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; export type GetSessionOptions = { optional?: boolean; diff --git a/packages/core-app-api/src/lib/subjects.ts b/packages/core-app-api/src/lib/subjects.ts index e122da819b..79c2c25675 100644 --- a/packages/core-app-api/src/lib/subjects.ts +++ b/packages/core-app-api/src/lib/subjects.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; // TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects. diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index f4b3fe12bb..9bd51c4795 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -9,7 +9,7 @@ import { ComponentType } from 'react'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderResult } from '@testing-library/react'; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 22dc950bd4..f6f4ac901a 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -33,6 +33,7 @@ "@backstage/core-plugin-api": "^0.1.11", "@backstage/test-utils-core": "^0.1.3", "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts index 20b6bd1724..01ee412182 100644 --- a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { ErrorApi, ErrorContext, Observable } from '@backstage/core-plugin-api'; +import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; type Options = { collect?: boolean; diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index ab5d26afc5..d928c35435 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { - Observable, - StorageApi, - StorageValueChange, -} from '@backstage/core-plugin-api'; +import { StorageApi, StorageValueChange } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; export type MockStorageBucket = { [key: string]: any }; diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index e28b8b0d32..06dccf4b7f 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { IconButton } from '@material-ui/core'; import { LinkProps } from '@backstage/core-components'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 3373641d16..404ba6893f 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -36,6 +36,7 @@ "@backstage/core-plugin-api": "^0.1.11", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", + "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts index 87d99c8904..46a8e63084 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, StorageApi } from '@backstage/core-plugin-api'; +import { StorageApi } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; import { performMigrationToTheNewBucket } from './migration'; import { StarredEntitiesApi } from './StarredEntitiesApi'; diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/StarredEntitiesApi.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/StarredEntitiesApi.ts index 4c4c68d66a..56c7664bfb 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/StarredEntitiesApi.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/StarredEntitiesApi.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { ApiRef, createApiRef, Observable } from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; /** * An API to store starred entities diff --git a/plugins/config-schema/api-report.md b/plugins/config-schema/api-report.md index 15c00acc18..efeb1646eb 100644 --- a/plugins/config-schema/api-report.md +++ b/plugins/config-schema/api-report.md @@ -7,7 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { RouteRef } from '@backstage/core-plugin-api'; import { Schema } from 'jsonschema'; diff --git a/plugins/config-schema/src/api/StaticSchemaLoader.ts b/plugins/config-schema/src/api/StaticSchemaLoader.ts index 8958f4cb5b..48a125f38e 100644 --- a/plugins/config-schema/src/api/StaticSchemaLoader.ts +++ b/plugins/config-schema/src/api/StaticSchemaLoader.ts @@ -18,7 +18,7 @@ import ObservableImpl from 'zen-observable'; import { ResponseError } from '@backstage/errors'; import { Schema } from 'jsonschema'; import { ConfigSchemaApi, ConfigSchemaResult } from './types'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; const DEFAULT_URL = 'config-schema.json'; diff --git a/plugins/config-schema/src/api/types.ts b/plugins/config-schema/src/api/types.ts index 464ef970fb..13cd4f602b 100644 --- a/plugins/config-schema/src/api/types.ts +++ b/plugins/config-schema/src/api/types.ts @@ -15,7 +15,8 @@ */ import { Schema } from 'jsonschema'; -import { createApiRef, Observable } from '@backstage/core-plugin-api'; +import { createApiRef } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; export interface ConfigSchemaResult { schema?: Schema; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 74abada9a8..740685e922 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -22,7 +22,7 @@ import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 1511899c65..1b531b652d 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -15,7 +15,7 @@ */ import { EntityName } from '@backstage/catalog-model'; -import { JsonObject, JsonValue } from '@backstage/types'; +import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { Field, FieldValidation } from '@rjsf/core'; @@ -25,7 +25,6 @@ import { createApiRef, DiscoveryApi, IdentityApi, - Observable, } from '@backstage/core-plugin-api'; export const scaffolderApiRef = createApiRef({ diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index 9eb1666db4..7414a35189 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -17,7 +17,8 @@ import { useImmerReducer } from 'use-immer'; import { useEffect } from 'react'; import { scaffolderApiRef, LogEvent } from '../../api'; import { ScaffolderTask, Status, TaskOutput } from '../../types'; -import { Subscription, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; +import { Subscription } from '@backstage/types'; type Step = { id: string; diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md index 0e1442b53f..d85d427c99 100644 --- a/plugins/shortcuts/api-report.md +++ b/plugins/shortcuts/api-report.md @@ -7,7 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; import { StorageApi } from '@backstage/core-plugin-api'; diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index c9afe49c5a..14dfd16896 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -24,6 +24,7 @@ "@backstage/core-components": "^0.7.1", "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/plugins/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts index c7802a0385..dcecc9945b 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -15,7 +15,8 @@ */ import { Shortcut } from '../types'; -import { createApiRef, Observable } from '@backstage/core-plugin-api'; +import { createApiRef } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; export const shortcutsApiRef = createApiRef({ id: 'plugin.shortcuts.api', From 8c93478a4eb4d2849a8123d07cc8e0f381b397e2 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Fri, 22 Oct 2021 10:22:54 +0100 Subject: [PATCH 087/245] Update api docs Signed-off-by: Nicolas Arnold --- .../src/providers/github/provider.ts | 2 +- yarn.lock | 307 +----------------- 2 files changed, 2 insertions(+), 307 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index ff718dde87..bc81e61a10 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -114,7 +114,7 @@ export class GithubAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, - state: await (await this.stateEncoder(req.state)).encodedState, + state: (await this.stateEncoder(req.state)).encodedState, }); } diff --git a/yarn.lock b/yarn.lock index b38d8cef4a..c905fbea6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -343,13 +343,6 @@ dependencies: "@babel/highlight" "^7.14.5" -"@babel/code-frame@^7.15.8": - version "7.15.8" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503" - integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg== - dependencies: - "@babel/highlight" "^7.14.5" - "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.4": version "7.14.4" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz#45720fe0cecf3fd42019e1d12cc3d27fadc98d58" @@ -360,11 +353,6 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.9.tgz#ac7996ceaafcf8f410119c8af0d1db4cf914a210" integrity sha512-p3QjZmMGHDGdpcwEYYWu7i7oJShJvtgMjJeb0W95PPhSm++3lm8YXYOh45Y6iCN9PkZLTZ7CIX5nFrp7pw7TXw== -"@babel/compat-data@^7.15.0": - version "7.15.0" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" - integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA== - "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -408,27 +396,6 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/core@^7.4.4": - version "7.15.8" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz#195b9f2bffe995d2c6c159e72fe525b4114e8c10" - integrity sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og== - dependencies: - "@babel/code-frame" "^7.15.8" - "@babel/generator" "^7.15.8" - "@babel/helper-compilation-targets" "^7.15.4" - "@babel/helper-module-transforms" "^7.15.8" - "@babel/helpers" "^7.15.4" - "@babel/parser" "^7.15.8" - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.6" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" - "@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.14.8", "@babel/generator@^7.14.9": version "7.14.9" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.14.9.tgz#23b19c597d38b4f7dc2e3fe42a69c88d9ecfaa16" @@ -447,15 +414,6 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.15.4", "@babel/generator@^7.15.8": - version "7.15.8" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz#fa56be6b596952ceb231048cf84ee499a19c0cd1" - integrity sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g== - dependencies: - "@babel/types" "^7.15.6" - jsesc "^2.5.1" - source-map "^0.5.0" - "@babel/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -506,16 +464,6 @@ browserslist "^4.16.6" semver "^6.3.0" -"@babel/helper-compilation-targets@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz#cf6d94f30fbefc139123e27dd6b02f65aeedb7b9" - integrity sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ== - dependencies: - "@babel/compat-data" "^7.15.0" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.16.6" - semver "^6.3.0" - "@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.14.0", "@babel/helper-create-class-features-plugin@^7.14.3": version "7.14.4" resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz#abf888d836a441abee783c75229279748705dc42" @@ -616,15 +564,6 @@ "@babel/template" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/helper-function-name@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc" - integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw== - dependencies: - "@babel/helper-get-function-arity" "^7.15.4" - "@babel/template" "^7.15.4" - "@babel/types" "^7.15.4" - "@babel/helper-get-function-arity@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" @@ -639,13 +578,6 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-get-function-arity@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" - integrity sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-hoist-variables@^7.13.0": version "7.13.16" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.16.tgz#1b1651249e94b51f8f0d33439843e33e39775b30" @@ -661,13 +593,6 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-hoist-variables@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df" - integrity sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-member-expression-to-functions@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72" @@ -682,13 +607,6 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-member-expression-to-functions@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz#bfd34dc9bba9824a4658b0317ec2fd571a51e6ef" - integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" @@ -703,13 +621,6 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-module-imports@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz#e18007d230632dea19b47853b984476e7b4e103f" - integrity sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.14.8": version "7.14.8" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49" @@ -738,20 +649,6 @@ "@babel/traverse" "^7.14.2" "@babel/types" "^7.14.2" -"@babel/helper-module-transforms@^7.15.4", "@babel/helper-module-transforms@^7.15.8": - version "7.15.8" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz#d8c0e75a87a52e374a8f25f855174786a09498b2" - integrity sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg== - dependencies: - "@babel/helper-module-imports" "^7.15.4" - "@babel/helper-replace-supers" "^7.15.4" - "@babel/helper-simple-access" "^7.15.4" - "@babel/helper-split-export-declaration" "^7.15.4" - "@babel/helper-validator-identifier" "^7.15.7" - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.6" - "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" @@ -766,13 +663,6 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-optimise-call-expression@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171" - integrity sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-plugin-utils@7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" @@ -826,16 +716,6 @@ "@babel/traverse" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/helper-replace-supers@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz#52a8ab26ba918c7f6dee28628b07071ac7b7347a" - integrity sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.15.4" - "@babel/helper-optimise-call-expression" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - "@babel/helper-simple-access@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" @@ -850,13 +730,6 @@ dependencies: "@babel/types" "^7.14.8" -"@babel/helper-simple-access@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz#ac368905abf1de8e9781434b635d8f8674bcc13b" - integrity sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-skip-transparent-expression-wrappers@^7.12.1": version "7.12.1" resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" @@ -885,13 +758,6 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-split-export-declaration@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz#aecab92dcdbef6a10aa3b62ab204b085f776e257" - integrity sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.0": version "7.14.0" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" @@ -902,11 +768,6 @@ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== -"@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -946,15 +807,6 @@ "@babel/traverse" "^7.14.8" "@babel/types" "^7.14.8" -"@babel/helpers@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz#5f40f02050a3027121a3cf48d497c05c555eaf43" - integrity sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ== - dependencies: - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - "@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": version "7.14.0" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" @@ -983,11 +835,6 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.14.9.tgz#596c1ad67608070058ebf8df50c1eaf65db895a4" integrity sha512-RdUTOseXJ8POjjOeEBEvNMIZU/nm4yu2rufRkcibzkkg7DmQvXU8v3M4Xk9G7uuI86CDGkKcuDWgioqZm+mScQ== -"@babel/parser@^7.15.4", "@babel/parser@^7.15.8": - version "7.15.8" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz#7bacdcbe71bdc3ff936d510c15dcea7cf0b99016" - integrity sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA== - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": version "7.13.12" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" @@ -1699,16 +1546,6 @@ "@babel/helper-simple-access" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.4.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz#8201101240eabb5a76c08ef61b2954f767b6b4c1" - integrity sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA== - dependencies: - "@babel/helper-module-transforms" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-simple-access" "^7.15.4" - babel-plugin-dynamic-import-node "^2.3.3" - "@babel/plugin-transform-modules-systemjs@^7.13.8": version "7.13.8" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" @@ -2259,15 +2096,6 @@ "@babel/parser" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/template@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194" - integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/parser" "^7.15.4" - "@babel/types" "^7.15.4" - "@babel/traverse@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz#689f0e4b4c08587ad26622832632735fb8c4e0c0" @@ -2312,21 +2140,6 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" - integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.15.4" - "@babel/helper-function-name" "^7.15.4" - "@babel/helper-hoist-variables" "^7.15.4" - "@babel/helper-split-export-declaration" "^7.15.4" - "@babel/parser" "^7.15.4" - "@babel/types" "^7.15.4" - debug "^4.1.0" - globals "^11.1.0" - "@babel/types@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz#8be1aa8f2c876da11a9cf650c0ecf656913ad611" @@ -2352,14 +2165,6 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" -"@babel/types@^7.15.4", "@babel/types@^7.15.6": - version "7.15.6" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" - integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== - dependencies: - "@babel/helper-validator-identifier" "^7.14.9" - to-fast-properties "^2.0.0" - "@backstage/catalog-client@^0.3.18": version "0.3.19" resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.3.19.tgz#109b0fde1cc2d9a306635f3775fc8f6174172b14" @@ -2370,117 +2175,6 @@ "@backstage/errors" "^0.1.2" cross-fetch "^3.0.6" -"@backstage/cli@^0.7.11": - version "0.7.16" - resolved "https://roadiehq.jfrog.io/artifactory/api/npm/backstage-all/@backstage/cli/-/cli-0.7.16.tgz#42cf317b39ec7fe45617cc493f84482a91af44d0" - integrity sha1-Qs8xeznsf+RWF8xJP4RIKpGvRNA= - dependencies: - "@babel/core" "^7.4.4" - "@babel/plugin-transform-modules-commonjs" "^7.4.4" - "@backstage/cli-common" "^0.1.4" - "@backstage/config" "^0.1.10" - "@backstage/config-loader" "^0.6.10" - "@hot-loader/react-dom" "^16.13.0" - "@lerna/package-graph" "^4.0.0" - "@lerna/project" "^4.0.0" - "@octokit/request" "^5.4.12" - "@rollup/plugin-commonjs" "^17.1.0" - "@rollup/plugin-json" "^4.0.2" - "@rollup/plugin-node-resolve" "^13.0.0" - "@rollup/plugin-yaml" "^3.0.0" - "@spotify/eslint-config-base" "^9.0.0" - "@spotify/eslint-config-react" "^10.0.0" - "@spotify/eslint-config-typescript" "^10.0.0" - "@sucrase/jest-plugin" "^2.1.1" - "@sucrase/webpack-loader" "^2.0.0" - "@svgr/plugin-jsx" "5.5.x" - "@svgr/plugin-svgo" "5.4.x" - "@svgr/rollup" "5.5.x" - "@svgr/webpack" "5.5.x" - "@types/webpack-env" "^1.15.2" - "@typescript-eslint/eslint-plugin" "^v4.30.0" - "@typescript-eslint/parser" "^v4.28.3" - "@yarnpkg/lockfile" "^1.1.0" - babel-plugin-dynamic-import-node "^2.3.3" - bfj "^7.0.2" - buffer "^6.0.3" - chalk "^4.0.0" - chokidar "^3.3.1" - commander "^6.1.0" - css-loader "^5.2.6" - dashify "^2.0.0" - diff "^5.0.0" - esbuild "^0.8.56" - eslint "^7.30.0" - eslint-config-prettier "^8.3.0" - eslint-formatter-friendly "^7.0.0" - eslint-plugin-import "^2.20.2" - eslint-plugin-jest "^24.1.0" - eslint-plugin-jsx-a11y "^6.2.1" - eslint-plugin-monorepo "^0.3.2" - eslint-plugin-react "^7.12.4" - eslint-plugin-react-hooks "^4.0.0" - express "^4.17.1" - fork-ts-checker-webpack-plugin "^4.0.5" - fs-extra "9.1.0" - glob "^7.1.7" - handlebars "^4.7.3" - html-webpack-plugin "^5.3.1" - inquirer "^7.0.4" - jest "^26.0.1" - jest-css-modules "^2.1.0" - jest-transform-yaml "^0.1.1" - json-schema "^0.3.0" - lodash "^4.17.21" - mini-css-extract-plugin "^2.4.2" - node-libs-browser "^2.2.1" - ora "^5.3.0" - postcss "^8.1.0" - process "^0.11.10" - react "^16.0.0" - react-dev-utils "^11.0.4" - react-hot-loader "^4.12.21" - recursive-readdir "^2.2.2" - replace-in-file "^6.0.0" - rollup "2.44.x" - rollup-plugin-dts "^3.0.1" - rollup-plugin-esbuild "2.6.x" - rollup-plugin-peer-deps-external "^2.2.2" - rollup-plugin-postcss "^4.0.0" - rollup-pluginutils "^2.8.2" - run-script-webpack-plugin "^0.0.11" - semver "^7.3.2" - style-loader "^1.2.1" - sucrase "^3.20.2" - tar "^6.1.2" - terser-webpack-plugin "^5.1.3" - ts-loader "^8.0.17" - typescript "^4.0.3" - util "^0.12.3" - webpack "^5.48.0" - webpack-dev-server "4.0.0-rc.0" - webpack-node-externals "^3.0.0" - yaml "^1.10.0" - yml-loader "^2.1.0" - yn "^4.0.0" - -"@backstage/config-loader@^0.6.10": - version "0.6.10" - resolved "https://roadiehq.jfrog.io/artifactory/api/npm/backstage-all/@backstage/config-loader/-/config-loader-0.6.10.tgz#8999ffbe4a628297ff72fdccd510d72b479a8e26" - integrity sha1-iZn/vkpigpf/cv3M1RDXK0eajiY= - dependencies: - "@backstage/cli-common" "^0.1.4" - "@backstage/config" "^0.1.9" - "@types/json-schema" "^7.0.6" - ajv "^7.0.3" - chokidar "^3.5.2" - fs-extra "9.1.0" - json-schema "^0.3.0" - json-schema-merge-allof "^0.8.1" - typescript-json-schema "^0.50.1" - yaml "^1.9.2" - yup "^0.32.9" - "@backstage/core-api@^0.2.23": version "0.2.23" resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.23.tgz#c0ec13407ff7c78d376eb18e9d8e1490d54d995b" @@ -20657,6 +20351,7 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: + encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" From 2a0ad49da095cbc471ca54e0e8a05d9ad66e7b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 22 Oct 2021 16:59:54 +0200 Subject: [PATCH 088/245] fix some of the ae-missing-release-tag warnings in core-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/core-components/api-report.md | 185 +----------------- .../components/AlertDisplay/AlertDisplay.tsx | 1 + .../src/components/Avatar/Avatar.tsx | 10 +- .../src/components/Button/Button.tsx | 3 + .../components/CodeSnippet/CodeSnippet.tsx | 4 + .../CopyTextButton/CopyTextButton.tsx | 3 + .../components/CreateButton/CreateButton.tsx | 4 + .../DependencyGraph/DefaultLabel.tsx | 2 + .../DependencyGraph/DefaultNode.tsx | 2 + .../DependencyGraph/DependencyGraph.tsx | 3 + .../src/components/DependencyGraph/Edge.tsx | 2 + .../src/components/DependencyGraph/Node.tsx | 1 + .../src/components/DependencyGraph/types.ts | 10 + .../DismissableBanner/DismissableBanner.tsx | 4 +- .../src/components/DismissableBanner/index.ts | 2 +- .../src/components/EmptyState/EmptyState.tsx | 2 + .../components/EmptyState/EmptyStateImage.tsx | 2 + .../src/components/ErrorPanel/ErrorPanel.tsx | 4 + .../FeatureCalloutCircular.tsx | 2 + .../HeaderIconLinkRow/HeaderIconLinkRow.tsx | 2 + .../HeaderIconLinkRow/IconLinkVertical.tsx | 2 + .../HorizontalScrollGrid.tsx | 2 + .../src/components/ProgressBars/Gauge.tsx | 2 + .../src/components/ProgressBars/GaugeCard.tsx | 2 + .../Select/static/ClosedDropdown.tsx | 1 + .../src/components/Table/Table.tsx | 1 + packages/core-components/src/icons/icons.tsx | 11 ++ .../src/layout/BottomLink/BottomLink.tsx | 3 + .../src/layout/Breadcrumbs/Breadcrumbs.tsx | 3 + .../src/layout/Content/Content.tsx | 1 + .../layout/ContentHeader/ContentHeader.tsx | 3 + .../layout/ErrorBoundary/ErrorBoundary.tsx | 2 + .../src/layout/ErrorPage/ErrorPage.tsx | 2 + .../src/layout/Header/Header.tsx | 2 + .../src/layout/HeaderLabel/HeaderLabel.tsx | 2 + .../src/layout/HeaderTabs/HeaderTabs.tsx | 3 + .../layout/HomepageTimer/HomepageTimer.tsx | 2 + .../src/layout/InfoCard/InfoCard.tsx | 6 +- .../src/layout/ItemCard/ItemCardGrid.tsx | 8 +- .../src/layout/ItemCard/ItemCardHeader.tsx | 8 +- .../src/layout/Sidebar/Intro.tsx | 2 + .../src/layout/SignInPage/customProvider.tsx | 2 + .../src/layout/TabbedCard/TabbedCard.tsx | 3 + .../src/overridableComponents.ts | 5 +- 44 files changed, 132 insertions(+), 194 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index dc8115287a..62db9d93fd 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -45,13 +45,9 @@ import { Theme } from '@material-ui/core/styles'; import { TooltipProps } from '@material-ui/core/Tooltip'; import { WithStyles } from '@material-ui/core/styles'; -// Warning: (ae-missing-release-tag) "AlertDisplay" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function AlertDisplay(_props: {}): JSX.Element | null; -// Warning: (ae-missing-release-tag) "Alignment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public enum Alignment { DOWN_LEFT = 'DL', @@ -60,18 +56,12 @@ enum Alignment { UP_RIGHT = 'UR', } -// Warning: (ae-missing-release-tag) "Avatar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function Avatar(props: AvatarProps): JSX.Element; -// Warning: (ae-missing-release-tag) "AvatarClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AvatarClassKey = 'avatar'; -// Warning: (ae-missing-release-tag) "AvatarProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface AvatarProps { customStyles?: CSSProperties; @@ -79,13 +69,10 @@ export interface AvatarProps { picture?: string; } -// Warning: (ae-missing-release-tag) "BackstageContentClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BackstageContentClassKey = 'root' | 'stretch' | 'noPadding'; // Warning: (ae-forgotten-export) The symbol "BackstageComponentsNameToClassKey" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "BackstageOverrides" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type BackstageOverrides = Overrides & { @@ -94,23 +81,15 @@ export type BackstageOverrides = Overrides & { >; }; -// Warning: (ae-missing-release-tag) "BoldHeaderClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BoldHeaderClassKey = 'root' | 'title' | 'subheader'; -// Warning: (ae-missing-release-tag) "BottomLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function BottomLink(props: BottomLinkProps): JSX.Element; -// Warning: (ae-missing-release-tag) "BottomLinkClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BottomLinkClassKey = 'root' | 'boxTitle' | 'arrow'; -// Warning: (ae-missing-release-tag) "BottomLinkProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BottomLinkProps = { link: string; @@ -119,75 +98,50 @@ export type BottomLinkProps = { }; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Breadcrumbs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Breadcrumbs(props: Props_21): JSX.Element; -// Warning: (ae-missing-release-tag) "BreadcrumbsClickableTextClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; -// Warning: (ae-missing-release-tag) "BreadcrumbsStyledBoxClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type BreadcrumbsStyledBoxClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "IconComponentProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "BrokenImageIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function BrokenImageIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ButtonType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function Button(props: ButtonProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ButtonProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ButtonProps = ButtonProps_2 & Omit; -// Warning: (ae-missing-release-tag) "CardActionsTopRightClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CardActionsTopRightClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "CardTabProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "CardTab" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function CardTab(props: PropsWithChildren): JSX.Element; -// Warning: (ae-missing-release-tag) "CardTabClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CardTabClassKey = 'root' | 'selected'; -// Warning: (ae-missing-release-tag) "CatalogIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function CatalogIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ChatIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function ChatIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ClosedDropdownClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ClosedDropdownClassKey = 'icon'; -// Warning: (ae-missing-release-tag) "CodeSnippet" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function CodeSnippet(props: CodeSnippetProps): JSX.Element; -// Warning: (ae-missing-release-tag) "CodeSnippetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface CodeSnippetProps { customStyle?: any; @@ -206,15 +160,12 @@ export interface CodeSnippetProps { export function Content(props: PropsWithChildren): JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ContentHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function ContentHeader( props: PropsWithChildren, ): JSX.Element; -// Warning: (ae-missing-release-tag) "ContentHeaderClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ContentHeaderClassKey = | 'container' @@ -223,13 +174,9 @@ export type ContentHeaderClassKey = | 'description' | 'title'; -// Warning: (ae-missing-release-tag) "CopyTextButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function CopyTextButton(props: CopyTextButtonProps): JSX.Element; -// Warning: (ae-missing-release-tag) "CopyTextButtonProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface CopyTextButtonProps { text: string; @@ -237,30 +184,20 @@ export interface CopyTextButtonProps { tooltipText?: string; } -// Warning: (ae-missing-release-tag) "CreateButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function CreateButton(props: CreateButtonProps): JSX.Element | null; -// Warning: (ae-missing-release-tag) "CreateButtonProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type CreateButtonProps = { title: string; } & Partial>; -// Warning: (ae-missing-release-tag) "CustomProviderClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CustomProviderClassKey = 'form' | 'button'; -// Warning: (ae-missing-release-tag) "DashboardIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function DashboardIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "DependencyEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public type DependencyEdge = T & { from: string; @@ -268,35 +205,23 @@ type DependencyEdge = T & { label?: string; }; -// Warning: (ae-missing-release-tag) "DependencyGraph" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function DependencyGraph( props: DependencyGraphProps, ): JSX.Element; -// Warning: (ae-missing-release-tag) "DependencyGraphDefaultLabelClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type DependencyGraphDefaultLabelClassKey = 'text'; -// Warning: (ae-missing-release-tag) "DependencyGraphDefaultNodeClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type DependencyGraphDefaultNodeClassKey = 'node' | 'text'; -// Warning: (ae-missing-release-tag) "DependencyGraphEdgeClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type DependencyGraphEdgeClassKey = 'path' | 'label'; -// Warning: (ae-missing-release-tag) "DependencyGraphNodeClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type DependencyGraphNodeClassKey = 'node'; -// Warning: (ae-missing-release-tag) "DependencyGraphProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface DependencyGraphProps extends React_2.SVGProps { @@ -341,15 +266,11 @@ declare namespace DependencyGraphTypes { } export { DependencyGraphTypes }; -// Warning: (ae-missing-release-tag) "DependencyNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public type DependencyNode = T & { id: string; }; -// Warning: (ae-missing-release-tag) "Direction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public enum Direction { BOTTOM_TOP = 'BT', @@ -359,15 +280,12 @@ enum Direction { } // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "DismissableBanner" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const DismissableBanner: (props: Props) => JSX.Element; -// Warning: (ae-missing-release-tag) "DismissbleBannerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export type DismissbleBannerClassKey = +export type DismissableBannerClassKey = | 'root' | 'topPosition' | 'icon' @@ -376,40 +294,28 @@ export type DismissbleBannerClassKey = | 'info' | 'error'; -// Warning: (ae-missing-release-tag) "DocsIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function DocsIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "EmailIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function EmailIcon(props: IconComponentProps): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EmptyState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function EmptyState(props: Props_2): JSX.Element; -// Warning: (ae-missing-release-tag) "EmptyStateClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type EmptyStateClassKey = 'root' | 'action' | 'imageContainer'; -// Warning: (ae-missing-release-tag) "EmptyStateImageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type EmptyStateImageClassKey = 'generalImg'; // Warning: (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ErrorBoundary" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const ErrorBoundary: ComponentClass; -// Warning: (ae-missing-release-tag) "ErrorBoundaryProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ErrorBoundaryProps = { slackChannel?: string | SlackChannel; @@ -417,30 +323,21 @@ export type ErrorBoundaryProps = { }; // Warning: (ae-forgotten-export) The symbol "IErrorPageProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ErrorPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function ErrorPage(props: IErrorPageProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ErrorPageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; -// Warning: (ae-missing-release-tag) "ErrorPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function ErrorPanel( props: PropsWithChildren, ): JSX.Element; -// Warning: (ae-missing-release-tag) "ErrorPanelClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ErrorPanelClassKey = 'text' | 'divider'; -// Warning: (ae-missing-release-tag) "ErrorPanelProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ErrorPanelProps = { error: Error; @@ -448,8 +345,6 @@ export type ErrorPanelProps = { title?: string; }; -// Warning: (ae-missing-release-tag) "FeatureCalloutCircleClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type FeatureCalloutCircleClassKey = | '@keyframes pulsateSlightly' @@ -461,58 +356,42 @@ export type FeatureCalloutCircleClassKey = | 'text'; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FeatureCalloutCircular" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function FeatureCalloutCircular( props: PropsWithChildren, ): JSX.Element; -// Warning: (ae-missing-release-tag) "FiltersContainerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Gauge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Gauge(props: Props_11): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "GaugeCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function GaugeCard(props: Props_10): JSX.Element; -// Warning: (ae-missing-release-tag) "GaugeCardClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type GaugeCardClassKey = 'root'; -// Warning: (ae-missing-release-tag) "GaugeClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; -// Warning: (ae-missing-release-tag) "GitHubIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function GitHubIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "GroupIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function GroupIcon(props: IconComponentProps): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Header" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Header(props: PropsWithChildren): JSX.Element; -// Warning: (ae-missing-release-tag) "HeaderClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type HeaderClassKey = | 'header' @@ -526,35 +405,26 @@ export type HeaderClassKey = | 'breadcrumbTitle'; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "HeaderIconLinkRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function HeaderIconLinkRow(props: Props_5): JSX.Element; -// Warning: (ae-missing-release-tag) "HeaderIconLinkRowClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type HeaderIconLinkRowClassKey = 'links'; // Warning: (ae-forgotten-export) The symbol "HeaderLabelProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "HeaderLabel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function HeaderLabel(props: HeaderLabelProps): JSX.Element; -// Warning: (ae-missing-release-tag) "HeaderLabelClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type HeaderLabelClassKey = 'root' | 'label' | 'value'; // Warning: (ae-forgotten-export) The symbol "HeaderTabsProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "HeaderTabs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function HeaderTabs(props: HeaderTabsProps): JSX.Element; -// Warning: (ae-missing-release-tag) "HeaderTabsClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type HeaderTabsClassKey = | 'tabsWrapper' @@ -562,26 +432,19 @@ export type HeaderTabsClassKey = | 'selected' | 'tabRoot'; -// Warning: (ae-missing-release-tag) "HelpIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function HelpIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "HomepageTimer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export function HomepageTimer(_props: {}): JSX.Element | null; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "HorizontalScrollGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function HorizontalScrollGrid( props: PropsWithChildren, ): JSX.Element; -// Warning: (ae-missing-release-tag) "HorizontalScrollGridClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type HorizontalScrollGridClassKey = | 'root' @@ -594,8 +457,6 @@ export type HorizontalScrollGridClassKey = | 'buttonLeft' | 'buttonRight'; -// Warning: (ae-missing-release-tag) "IconLinkVerticalClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type IconLinkVerticalClassKey = | 'link' @@ -618,13 +479,10 @@ export type IconLinkVerticalProps = { }; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "InfoCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function InfoCard(props: Props_16): JSX.Element; -// Warning: (ae-missing-release-tag) "InfoCardClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type InfoCardClassKey = | 'noPadding' @@ -635,13 +493,10 @@ export type InfoCardClassKey = | 'headerAction' | 'headerContent'; -// Warning: (ae-missing-release-tag) "InfoCardVariants" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; // Warning: (ae-forgotten-export) The symbol "IntroCardProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "IntroCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function IntroCard(props: IntroCardProps): JSX.Element; @@ -654,48 +509,26 @@ export function IntroCard(props: IntroCardProps): JSX.Element; // @public @deprecated export function ItemCard(props: ItemCardProps): JSX.Element; -// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-html-tag-missing-string) The HTML element has an invalid attribute: Expecting an HTML string starting with a single-quote or double-quote character -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (ae-missing-release-tag) "ItemCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function ItemCardGrid(props: ItemCardGridProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ItemCardGridClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ItemCardGridClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "styles" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ItemCardGridProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type ItemCardGridProps = Partial> & { children?: React_2.ReactNode; }; -// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-html-tag-missing-string) The HTML element has an invalid attribute: Expecting an HTML string starting with a single-quote or double-quote character -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (ae-missing-release-tag) "ItemCardHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function ItemCardHeader(props: ItemCardHeaderProps): JSX.Element; -// Warning: (ae-missing-release-tag) "ItemCardHeaderClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ItemCardHeaderClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "styles" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ItemCardHeaderProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type ItemCardHeaderProps = Partial> & { @@ -704,8 +537,6 @@ export type ItemCardHeaderProps = Partial> & { children?: React_2.ReactNode; }; -// Warning: (ae-missing-release-tag) "LabelPosition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public enum LabelPosition { // (undocumented) @@ -1254,8 +1085,6 @@ export const SidebarDivider: React_2.ComponentType< // @public (undocumented) export function SidebarIntro(_props: {}): JSX.Element | null; -// Warning: (ae-missing-release-tag) "SidebarIntroClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type SidebarIntroClassKey = | 'introCard' @@ -2427,8 +2256,6 @@ export function useQueryParamState( debounceTime?: number, ): [T | undefined, SetQueryParams]; -// Warning: (ae-missing-release-tag) "UserIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function UserIcon(props: IconComponentProps): JSX.Element; @@ -2437,8 +2264,6 @@ export function UserIcon(props: IconComponentProps): JSX.Element; // @public (undocumented) export function useSupportConfig(): SupportConfig; -// Warning: (ae-missing-release-tag) "WarningIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function WarningIcon(props: IconComponentProps): JSX.Element; @@ -2472,9 +2297,9 @@ export type WarningPanelClassKey = // Warnings were encountered during analysis: // -// src/components/DependencyGraph/types.d.ts:14:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" -// src/components/DependencyGraph/types.d.ts:18:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:16:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:20:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts -// src/components/Table/Table.d.ts:19:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts -// src/layout/ErrorBoundary/ErrorBoundary.d.ts:7:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts +// src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts +// src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx index 3a7ec392ee..5d90979557 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx @@ -25,6 +25,7 @@ import pluralize from 'pluralize'; /** * Displays alerts from {@link @backstage/core-plugin-api#AlertApi} * + * @public * @remarks * * Shown as SnackBar at the top of the page diff --git a/packages/core-components/src/components/Avatar/Avatar.tsx b/packages/core-components/src/components/Avatar/Avatar.tsx index 32ce92b46b..304ee574d2 100644 --- a/packages/core-components/src/components/Avatar/Avatar.tsx +++ b/packages/core-components/src/components/Avatar/Avatar.tsx @@ -18,6 +18,7 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import MaterialAvatar from '@material-ui/core/Avatar'; import { extractInitials, stringToColor } from './utils'; +/** @public */ export type AvatarClassKey = 'avatar'; const useStyles = makeStyles( @@ -36,7 +37,9 @@ const useStyles = makeStyles( ); /** - * Properties for {@link Avatar} + * Properties for {@link Avatar}. + * + * @public */ export interface AvatarProps { /** @@ -56,9 +59,10 @@ export interface AvatarProps { /** * Component rendering an Avatar * - * @remarks + * @public + * @remarks * - * Based on https://v4.mui.com/components/avatars/#avatar with some styling adjustment and two-letter initials + * Based on https://v4.mui.com/components/avatars/#avatar with some styling adjustment and two-letter initials */ export function Avatar(props: AvatarProps) { const { displayName, picture, customStyles } = props; diff --git a/packages/core-components/src/components/Button/Button.tsx b/packages/core-components/src/components/Button/Button.tsx index d5c59c3169..81c8ee28a5 100644 --- a/packages/core-components/src/components/Button/Button.tsx +++ b/packages/core-components/src/components/Button/Button.tsx @@ -23,6 +23,7 @@ import { Link, LinkProps } from '../Link'; /** * Properties for {@link Button} * + * @public * @remarks * * See {@link https://v4.mui.com/api/button/#props | Material-UI Button Props} for all properties @@ -33,6 +34,7 @@ export type ButtonProps = MaterialButtonProps & /** * Thin wrapper on top of material-ui's {@link https://v4.mui.com/components/buttons/ | Button} component * + * @public * @remarks * * Makes the Button to utilise react-router @@ -46,6 +48,7 @@ const LinkWrapper = React.forwardRef((props, ref) => ( )); +/** @public */ const ActualButton = React.forwardRef((props, ref) => ( )) as { (props: ButtonProps): JSX.Element }; diff --git a/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx index e1c407276b..ed78b2c590 100644 --- a/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx @@ -24,6 +24,8 @@ import docco from 'react-syntax-highlighter/dist/esm/styles/hljs/docco'; /** * Properties for {@link CodeSnippet} + * + * @public */ export interface CodeSnippetProps { /** @@ -67,6 +69,8 @@ export interface CodeSnippetProps { /** * Thin wrapper on top of {@link https://react-syntax-highlighter.github.io/react-syntax-highlighter/ | react-syntax-highlighter} * providing consistent theming and copy code button + * + * @public */ export function CodeSnippet(props: CodeSnippetProps) { const { diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx index 3986b8fa43..fdf0868197 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx @@ -23,6 +23,8 @@ import { useCopyToClipboard } from 'react-use'; /** * Properties for {@link CopyTextButton} + * + * @public */ export interface CopyTextButtonProps { /** @@ -50,6 +52,7 @@ export interface CopyTextButtonProps { /** * Copy text button with visual feedback * + * @public * @remarks * * Visual feedback takes form of: diff --git a/packages/core-components/src/components/CreateButton/CreateButton.tsx b/packages/core-components/src/components/CreateButton/CreateButton.tsx index 074ee9fc9f..d5d7080851 100644 --- a/packages/core-components/src/components/CreateButton/CreateButton.tsx +++ b/packages/core-components/src/components/CreateButton/CreateButton.tsx @@ -24,6 +24,8 @@ import AddCircleOutline from '@material-ui/icons/AddCircleOutline'; /** * Properties for {@link CreateButton} + * + * @public */ export type CreateButtonProps = { title: string; @@ -31,6 +33,8 @@ export type CreateButtonProps = { /** * Responsive Button giving consistent UX for creation of different things + * + * @public */ export function CreateButton(props: CreateButtonProps) { const { title, to } = props; diff --git a/packages/core-components/src/components/DependencyGraph/DefaultLabel.tsx b/packages/core-components/src/components/DependencyGraph/DefaultLabel.tsx index 7aa245fc1f..d04bf78869 100644 --- a/packages/core-components/src/components/DependencyGraph/DefaultLabel.tsx +++ b/packages/core-components/src/components/DependencyGraph/DefaultLabel.tsx @@ -19,6 +19,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import { BackstageTheme } from '@backstage/theme'; import { RenderLabelProps } from './types'; +/** @public */ export type DependencyGraphDefaultLabelClassKey = 'text'; const useStyles = makeStyles( @@ -30,6 +31,7 @@ const useStyles = makeStyles( { name: 'BackstageDependencyGraphDefaultLabel' }, ); +/** @public */ export function DefaultLabel({ edge: { label } }: RenderLabelProps) { const classes = useStyles(); return ( diff --git a/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx b/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx index 6e33148aec..b46aeb85e3 100644 --- a/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx +++ b/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx @@ -19,6 +19,7 @@ import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; import { RenderNodeProps } from './types'; +/** @public */ export type DependencyGraphDefaultNodeClassKey = 'node' | 'text'; const useStyles = makeStyles( @@ -34,6 +35,7 @@ const useStyles = makeStyles( { name: 'BackstageDependencyGraphDefaultNode' }, ); +/** @public */ export function DefaultNode({ node: { id } }: RenderNodeProps) { const classes = useStyles(); const [width, setWidth] = React.useState(0); diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 1c3a4fce96..49f209d04e 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -38,6 +38,7 @@ import { ARROW_MARKER_ID } from './constants'; /** * Properties of {@link DependencyGraph} * + * @public * @remarks * and are useful when rendering custom or edge labels */ @@ -167,6 +168,8 @@ const WORKSPACE_ID = 'workspace'; /** * Graph component used to visualize relations between entities + * + * @public */ export function DependencyGraph( props: DependencyGraphProps, diff --git a/packages/core-components/src/components/DependencyGraph/Edge.tsx b/packages/core-components/src/components/DependencyGraph/Edge.tsx index fd4d92b834..cc6a044beb 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.tsx @@ -41,6 +41,7 @@ export type EdgeProperties = { }; export type GraphEdge = DependencyEdge & dagre.GraphEdge & EdgeProperties; +/** @public */ export type DependencyGraphEdgeClassKey = 'path' | 'label'; const useStyles = makeStyles( @@ -60,6 +61,7 @@ const useStyles = makeStyles( type EdgePoint = dagre.GraphEdge['points'][0]; +/** @public */ export type EdgeComponentProps = { id: dagre.Edge; edge: GraphEdge; diff --git a/packages/core-components/src/components/DependencyGraph/Node.tsx b/packages/core-components/src/components/DependencyGraph/Node.tsx index 01d518b554..505f9e61c7 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.tsx @@ -21,6 +21,7 @@ import { RenderNodeFunction, RenderNodeProps, DependencyNode } from './types'; import { NODE_TEST_ID } from './constants'; import dagre from 'dagre'; +/** @public */ export type DependencyGraphNodeClassKey = 'node'; const useStyles = makeStyles( diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index eab793685b..4ae6057bd7 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -24,6 +24,8 @@ import React from 'react'; /** * Edge of {@link DependencyGraph} + * + * @public */ export type DependencyEdge = T & { /** @@ -54,6 +56,8 @@ export type RenderLabelFunction = ( /** * Node of {@link DependencyGraph} + * + * @public */ export type DependencyNode = T & { id: string; @@ -73,6 +77,8 @@ export type RenderNodeFunction = ( /** * Graph direction + * + * @public */ export enum Direction { /** @@ -95,6 +101,8 @@ export enum Direction { /** * Node alignment + * + * @public */ export enum Alignment { /** @@ -139,6 +147,8 @@ export enum Ranker { /** * Position of label in relation to the edge + * + * @public */ export enum LabelPosition { LEFT = 'l', diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index b2d0c68d17..f922689ef4 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -25,7 +25,8 @@ import SnackbarContent from '@material-ui/core/SnackbarContent'; import IconButton from '@material-ui/core/IconButton'; import Close from '@material-ui/icons/Close'; -export type DismissbleBannerClassKey = +/** @public */ +export type DismissableBannerClassKey = | 'root' | 'topPosition' | 'icon' @@ -83,6 +84,7 @@ type Props = { fixed?: boolean; }; +/** @public */ export const DismissableBanner = (props: Props) => { const { variant, message, id, fixed = false } = props; const classes = useStyles(); diff --git a/packages/core-components/src/components/DismissableBanner/index.ts b/packages/core-components/src/components/DismissableBanner/index.ts index 26bebfed59..b760c4d8ce 100644 --- a/packages/core-components/src/components/DismissableBanner/index.ts +++ b/packages/core-components/src/components/DismissableBanner/index.ts @@ -15,4 +15,4 @@ */ export { DismissableBanner } from './DismissableBanner'; -export type { DismissbleBannerClassKey } from './DismissableBanner'; +export type { DismissableBannerClassKey } from './DismissableBanner'; diff --git a/packages/core-components/src/components/EmptyState/EmptyState.tsx b/packages/core-components/src/components/EmptyState/EmptyState.tsx index a7298c4192..5abb41f48b 100644 --- a/packages/core-components/src/components/EmptyState/EmptyState.tsx +++ b/packages/core-components/src/components/EmptyState/EmptyState.tsx @@ -20,6 +20,7 @@ import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import { EmptyStateImage } from './EmptyStateImage'; +/** @public */ export type EmptyStateClassKey = 'root' | 'action' | 'imageContainer'; const useStyles = makeStyles( @@ -45,6 +46,7 @@ type Props = { action?: JSX.Element; }; +/** @public */ export function EmptyState(props: Props) { const { title, description, missing, action } = props; const classes = useStyles(); diff --git a/packages/core-components/src/components/EmptyState/EmptyStateImage.tsx b/packages/core-components/src/components/EmptyState/EmptyStateImage.tsx index f82d8e6e57..0200e3a39b 100644 --- a/packages/core-components/src/components/EmptyState/EmptyStateImage.tsx +++ b/packages/core-components/src/components/EmptyState/EmptyStateImage.tsx @@ -25,6 +25,7 @@ type Props = { missing: 'field' | 'info' | 'content' | 'data'; }; +/** @public */ export type EmptyStateImageClassKey = 'generalImg'; const useStyles = makeStyles( @@ -41,6 +42,7 @@ const useStyles = makeStyles( { name: 'BackstageEmptyStateImage' }, ); +/** @public */ export const EmptyStateImage = ({ missing }: Props) => { const classes = useStyles(); switch (missing) { diff --git a/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx b/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx index 3ef35d6eaa..02520d2c5c 100644 --- a/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx +++ b/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx @@ -22,6 +22,7 @@ import React, { PropsWithChildren } from 'react'; import { CopyTextButton } from '../CopyTextButton'; import { WarningPanel } from '../WarningPanel'; +/** @public */ export type ErrorPanelClassKey = 'text' | 'divider'; const useStyles = makeStyles( @@ -91,6 +92,7 @@ const ErrorList = ({ ); }; +/** @public */ export type ErrorPanelProps = { error: Error; defaultExpanded?: boolean; @@ -99,6 +101,8 @@ export type ErrorPanelProps = { /** * Renders a warning panel as the effect of an error. + * + * @public */ export function ErrorPanel(props: PropsWithChildren) { const { title, error, defaultExpanded, children } = props; diff --git a/packages/core-components/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx b/packages/core-components/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx index 3152d701c8..eb17641d89 100644 --- a/packages/core-components/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx +++ b/packages/core-components/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx @@ -29,6 +29,7 @@ import { createPortal } from 'react-dom'; import { usePortal } from './lib/usePortal'; import { useShowCallout } from './lib/useShowCallout'; +/** @public */ export type FeatureCalloutCircleClassKey = | '@keyframes pulsateSlightly' | '@keyframes pulsateAndFade' @@ -107,6 +108,7 @@ type Placement = { textWidth: number; }; +/** @public */ export function FeatureCalloutCircular(props: PropsWithChildren) { const { featureId, title, description, children } = props; const { show, hide } = useShowCallout(featureId); diff --git a/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx b/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx index 4e83976a96..82ba37c372 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { IconLinkVertical, IconLinkVerticalProps } from './IconLinkVertical'; import { makeStyles } from '@material-ui/core/styles'; +/** @public */ export type HeaderIconLinkRowClassKey = 'links'; const useStyles = makeStyles( @@ -36,6 +37,7 @@ type Props = { links: IconLinkVerticalProps[]; }; +/** @public */ export function HeaderIconLinkRow(props: Props) { const { links } = props; const classes = useStyles(); diff --git a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx index 421147753b..2816603f7f 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx @@ -30,6 +30,7 @@ export type IconLinkVerticalProps = { title?: string; }; +/** @public */ export type IconLinkVerticalClassKey = | 'link' | 'disabled' @@ -65,6 +66,7 @@ const useIconStyles = makeStyles( { name: 'BackstageIconLinkVertical' }, ); +/** @public */ export function IconLinkVertical({ color = 'primary', disabled = false, diff --git a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx index 483d27d657..2be702a754 100644 --- a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx +++ b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx @@ -56,6 +56,7 @@ type Props = { minScrollDistance?: number; // limits how small steps the scroll can take in px }; +/** @public */ export type HorizontalScrollGridClassKey = | 'root' | 'container' @@ -197,6 +198,7 @@ function useSmoothScroll( return setScrollTarget; } +/** @public */ export function HorizontalScrollGrid(props: PropsWithChildren) { const { scrollStep = 100, diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 2f6973e4d8..83e8d05b16 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -19,6 +19,7 @@ import { BackstageTheme } from '@backstage/theme'; import { Circle } from 'rc-progress'; import React from 'react'; +/** @public */ export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; const useStyles = makeStyles( @@ -82,6 +83,7 @@ export function getProgressColor( return palette.status.ok; } +/** @public */ export function Gauge(props: Props) { const classes = useStyles(props); const theme = useTheme(); diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx index 5c5a825921..fe17de30cd 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx @@ -30,6 +30,7 @@ type Props = { deepLink?: BottomLinkProps; }; +/** @public */ export type GaugeCardClassKey = 'root'; const useStyles = makeStyles( @@ -42,6 +43,7 @@ const useStyles = makeStyles( { name: 'BackstageGaugeCard' }, ); +/** @public */ export function GaugeCard(props: Props) { const classes = useStyles(props); const { title, subheader, progress, inverse, deepLink, variant } = props; diff --git a/packages/core-components/src/components/Select/static/ClosedDropdown.tsx b/packages/core-components/src/components/Select/static/ClosedDropdown.tsx index eb93f5a354..7b0516d017 100644 --- a/packages/core-components/src/components/Select/static/ClosedDropdown.tsx +++ b/packages/core-components/src/components/Select/static/ClosedDropdown.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { makeStyles, createStyles } from '@material-ui/core/styles'; import SvgIcon from '@material-ui/core/SvgIcon'; +/** @public */ export type ClosedDropdownClassKey = 'icon'; const useStyles = makeStyles( diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 234b64ec54..f433e7cdc5 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -132,6 +132,7 @@ const StyledMTableToolbar = withStyles( { name: 'BackstageTableToolbar' }, )(MTableToolbar); +/** @public */ export type FiltersContainerClassKey = 'root' | 'title'; const useFilterStyles = makeStyles( diff --git a/packages/core-components/src/icons/icons.tsx b/packages/core-components/src/icons/icons.tsx index 7a9f7f9832..7d5497f7e2 100644 --- a/packages/core-components/src/icons/icons.tsx +++ b/packages/core-components/src/icons/icons.tsx @@ -27,36 +27,47 @@ function useSystemIcon(key: string, props: IconComponentProps) { } // Should match the list of overridable system icon keys in @backstage/core-app-api +/** @public */ export function BrokenImageIcon(props: IconComponentProps) { return useSystemIcon('brokenImage', props); } +/** @public */ export function CatalogIcon(props: IconComponentProps) { return useSystemIcon('catalog', props); } +/** @public */ export function ChatIcon(props: IconComponentProps) { return useSystemIcon('chat', props); } +/** @public */ export function DashboardIcon(props: IconComponentProps) { return useSystemIcon('dashboard', props); } +/** @public */ export function DocsIcon(props: IconComponentProps) { return useSystemIcon('docs', props); } +/** @public */ export function EmailIcon(props: IconComponentProps) { return useSystemIcon('email', props); } +/** @public */ export function GitHubIcon(props: IconComponentProps) { return useSystemIcon('github', props); } +/** @public */ export function GroupIcon(props: IconComponentProps) { return useSystemIcon('group', props); } +/** @public */ export function HelpIcon(props: IconComponentProps) { return useSystemIcon('help', props); } +/** @public */ export function UserIcon(props: IconComponentProps) { return useSystemIcon('user', props); } +/** @public */ export function WarningIcon(props: IconComponentProps) { return useSystemIcon('warning', props); } diff --git a/packages/core-components/src/layout/BottomLink/BottomLink.tsx b/packages/core-components/src/layout/BottomLink/BottomLink.tsx index e097724385..f7c644bedf 100644 --- a/packages/core-components/src/layout/BottomLink/BottomLink.tsx +++ b/packages/core-components/src/layout/BottomLink/BottomLink.tsx @@ -23,6 +23,7 @@ import { BackstageTheme } from '@backstage/theme'; import Box from '@material-ui/core/Box'; import { Link } from '../../components/Link'; +/** @public */ export type BottomLinkClassKey = 'root' | 'boxTitle' | 'arrow'; const useStyles = makeStyles( @@ -42,12 +43,14 @@ const useStyles = makeStyles( { name: 'BackstageBottomLink' }, ); +/** @public */ export type BottomLinkProps = { link: string; title: string; onClick?: (event: React.MouseEvent) => void; }; +/** @public */ export function BottomLink(props: BottomLinkProps) { const { link, title, onClick } = props; const classes = useStyles(); diff --git a/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx b/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx index 4423c5c0b7..b0b2d6924f 100644 --- a/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx @@ -25,6 +25,7 @@ import React, { ComponentProps, Fragment } from 'react'; type Props = ComponentProps; +/** @public */ export type BreadcrumbsClickableTextClassKey = 'root'; const ClickableText = withStyles( @@ -37,6 +38,7 @@ const ClickableText = withStyles( { name: 'BackstageBreadcrumbsClickableText' }, )(Typography); +/** @public */ export type BreadcrumbsStyledBoxClassKey = 'root'; const StyledBox = withStyles( @@ -49,6 +51,7 @@ const StyledBox = withStyles( { name: 'BackstageBreadcrumbsStyledBox' }, )(Box); +/** @public */ export function Breadcrumbs(props: Props) { const { children, ...restProps } = props; const [anchorEl, setAnchorEl] = React.useState( diff --git a/packages/core-components/src/layout/Content/Content.tsx b/packages/core-components/src/layout/Content/Content.tsx index 122eb07e2a..b5b1300796 100644 --- a/packages/core-components/src/layout/Content/Content.tsx +++ b/packages/core-components/src/layout/Content/Content.tsx @@ -18,6 +18,7 @@ import React, { PropsWithChildren } from 'react'; import classNames from 'classnames'; import { Theme, makeStyles } from '@material-ui/core/styles'; +/** @public */ export type BackstageContentClassKey = 'root' | 'stretch' | 'noPadding'; const useStyles = makeStyles( diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx index b4a80ac4b1..2a8785514d 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx @@ -23,6 +23,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import { Helmet } from 'react-helmet'; +/** @public */ export type ContentHeaderClassKey = | 'container' | 'leftItemsBox' @@ -93,6 +94,7 @@ type ContentHeaderProps = { textAlign?: 'left' | 'right' | 'center'; }; +/** @public */ export function ContentHeader(props: PropsWithChildren) { const { description, @@ -108,6 +110,7 @@ export function ContentHeader(props: PropsWithChildren) { ) : ( ); + return ( <> diff --git a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx index b8dfd7ff3a..39ad82babd 100644 --- a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx @@ -23,6 +23,7 @@ type SlackChannel = { href?: string; }; +/** @public */ export type ErrorBoundaryProps = { slackChannel?: string | SlackChannel; onError?: (error: Error, errorInfo: string) => null; @@ -51,6 +52,7 @@ const SlackLink = (props: { slackChannel?: string | SlackChannel }) => { ); }; +/** @public */ export const ErrorBoundary: ComponentClass< ErrorBoundaryProps, State diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 46257f85bb..3d55307c81 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -30,6 +30,7 @@ interface IErrorPageProps { additionalInfo?: string; } +/** @public */ export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; const useStyles = makeStyles( @@ -54,6 +55,7 @@ const useStyles = makeStyles( { name: 'BackstageErrorPage' }, ); +/** @public */ export function ErrorPage(props: IErrorPageProps) { const { status, statusMessage, additionalInfo } = props; const classes = useStyles(); diff --git a/packages/core-components/src/layout/Header/Header.tsx b/packages/core-components/src/layout/Header/Header.tsx index aed9e9f8e4..40495ca0e0 100644 --- a/packages/core-components/src/layout/Header/Header.tsx +++ b/packages/core-components/src/layout/Header/Header.tsx @@ -26,6 +26,7 @@ import { Helmet } from 'react-helmet'; import { Link } from '../../components/Link'; import { Breadcrumbs } from '../Breadcrumbs'; +/** @public */ export type HeaderClassKey = | 'header' | 'leftItemsBox' @@ -190,6 +191,7 @@ const SubtitleFragment = ({ classes, subtitle }: SubtitleFragmentProps) => { ); }; +/** @public */ export function Header(props: PropsWithChildren) { const { children, diff --git a/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx b/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx index 3b9bde0f62..b4576a27a9 100644 --- a/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx +++ b/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx @@ -20,6 +20,7 @@ import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import React from 'react'; +/** @public */ export type HeaderLabelClassKey = 'root' | 'label' | 'value'; const useStyles = makeStyles( @@ -59,6 +60,7 @@ type HeaderLabelProps = { url?: string; }; +/** @public */ export function HeaderLabel(props: HeaderLabelProps) { const { label, value, url } = props; const classes = useStyles(); diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx index dd740d1c53..00dd3e9b95 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx @@ -22,6 +22,7 @@ import { makeStyles } from '@material-ui/core/styles'; import TabUI, { TabProps } from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; +/** @public */ export type HeaderTabsClassKey = | 'tabsWrapper' | 'defaultTab' @@ -66,6 +67,8 @@ type HeaderTabsProps = { onChange?: (index: number) => void; selectedIndex?: number; }; + +/** @public */ export function HeaderTabs(props: HeaderTabsProps) { const { tabs, onChange, selectedIndex } = props; const [selectedTab, setSelectedTab] = useState(selectedIndex ?? 0); diff --git a/packages/core-components/src/layout/HomepageTimer/HomepageTimer.tsx b/packages/core-components/src/layout/HomepageTimer/HomepageTimer.tsx index a825d52c94..71e764987b 100644 --- a/packages/core-components/src/layout/HomepageTimer/HomepageTimer.tsx +++ b/packages/core-components/src/layout/HomepageTimer/HomepageTimer.tsx @@ -69,6 +69,8 @@ function getTimes(configApi: ConfigApi) { /** * Please use the HeaderWorldClock in the home plugin + * + * @public * @deprecated in favor of the HeaderWorldClock which is found in the to home plugin */ export function HomepageTimer(_props: {}) { diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 0b9c4c7dcf..8bedca1013 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -25,6 +25,7 @@ import classNames from 'classnames'; import { ErrorBoundary, ErrorBoundaryProps } from '../ErrorBoundary'; import { BottomLink, BottomLinkProps } from '../BottomLink'; +/** @public */ export type InfoCardClassKey = | 'noPadding' | 'header' @@ -58,6 +59,7 @@ const useStyles = makeStyles( { name: 'BackstageInfoCard' }, ); +/** @public */ export type CardActionsTopRightClassKey = 'root'; const CardActionsTopRight = withStyles( @@ -99,6 +101,7 @@ const VARIANT_STYLES = { }, }; +/** @public */ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; /** @@ -117,7 +120,7 @@ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; * When the InfoCard is displayed as a grid item within a grid, you may want items to have the same height for all items. * Set to the 'gridItem' variant to display the InfoCard with full height suitable for Grid: * - * ... + * `...` */ type Props = { title?: ReactNode; @@ -141,6 +144,7 @@ type Props = { titleTypographyProps?: object; }; +/** @public */ export function InfoCard(props: Props): JSX.Element { const { title, diff --git a/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx b/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx index b4fe504c31..b27d224418 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx @@ -22,6 +22,7 @@ import { } from '@material-ui/core/styles'; import React from 'react'; +/** @public */ export type ItemCardGridClassKey = 'root'; const styles = (theme: Theme) => @@ -36,6 +37,7 @@ const styles = (theme: Theme) => const useStyles = makeStyles(styles, { name: 'BackstageItemCardGrid' }); +/** @public */ export type ItemCardGridProps = Partial> & { /** * The Card items of the grid. @@ -51,12 +53,12 @@ export type ItemCardGridProps = Partial> & { * * Styles for the grid can be overridden using the `classes` prop, e.g.: * - * - * - * + * `` * * This can be useful for e.g. overriding gridTemplateColumns to adapt the * minimum size of the cells to fit the content better. + * + * @public */ export function ItemCardGrid(props: ItemCardGridProps) { const { children, ...otherProps } = props; diff --git a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx index c604f24e3f..3209848ecb 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx @@ -19,6 +19,7 @@ import Typography from '@material-ui/core/Typography'; import React from 'react'; import { BackstageTheme } from '@backstage/theme'; +/** @public */ export type ItemCardHeaderClassKey = 'root'; const styles = (theme: BackstageTheme) => @@ -34,6 +35,7 @@ const styles = (theme: BackstageTheme) => const useStyles = makeStyles(styles, { name: 'BackstageItemCardHeader' }); +/** @public */ export type ItemCardHeaderProps = Partial> & { /** * A large title to show in the header, providing the main heading. @@ -66,9 +68,9 @@ export type ItemCardHeaderProps = Partial> & { * * Styles for the header can be overridden using the `classes` prop, e.g.: * - * - * - * + * `` + * + * @public */ export function ItemCardHeader(props: ItemCardHeaderProps) { const { title, subtitle, children } = props; diff --git a/packages/core-components/src/layout/Sidebar/Intro.tsx b/packages/core-components/src/layout/Sidebar/Intro.tsx index 8086898e64..63b8db14ed 100644 --- a/packages/core-components/src/layout/Sidebar/Intro.tsx +++ b/packages/core-components/src/layout/Sidebar/Intro.tsx @@ -29,6 +29,7 @@ import { } from './config'; import { SidebarDivider } from './Items'; +/** @public */ export type SidebarIntroClassKey = | 'introCard' | 'introDismiss' @@ -87,6 +88,7 @@ type IntroCardProps = { onClose: () => void; }; +/** @public */ export function IntroCard(props: IntroCardProps) { const classes = useStyles(); const { text, onClose } = props; diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index 3be375ed3d..da1848ac05 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -30,6 +30,7 @@ import { GridItem } from './styles'; // accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3) const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i; +/** @public */ export type CustomProviderClassKey = 'form' | 'button'; const useFormStyles = makeStyles( @@ -58,6 +59,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => { ...rest, }; }; + const Component: ProviderComponent = ({ onResult }) => { const classes = useFormStyles(); const { register, handleSubmit, formState } = useForm({ diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx index f9ec755e00..d791629246 100644 --- a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx +++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx @@ -46,6 +46,7 @@ const useTabsStyles = makeStyles( { name: 'BackstageTabbedCard' }, ); +/** @public */ export type BoldHeaderClassKey = 'root' | 'title' | 'subheader'; const BoldHeader = withStyles( @@ -120,6 +121,7 @@ export function TabbedCard(props: PropsWithChildren) { ); } +/** @public */ export type CardTabClassKey = 'root' | 'selected'; const useCardTabStyles = makeStyles( @@ -147,6 +149,7 @@ type CardTabProps = TabProps & { children: ReactNode; }; +/** @public */ export function CardTab(props: PropsWithChildren) { const { children, ...restProps } = props; const classes = useCardTabStyles(); diff --git a/packages/core-components/src/overridableComponents.ts b/packages/core-components/src/overridableComponents.ts index b3186ae48d..af35445e24 100644 --- a/packages/core-components/src/overridableComponents.ts +++ b/packages/core-components/src/overridableComponents.ts @@ -22,7 +22,7 @@ import { DependencyGraphDefaultNodeClassKey, DependencyGraphEdgeClassKey, DependencyGraphNodeClassKey, - DismissbleBannerClassKey, + DismissableBannerClassKey, EmptyStateClassKey, EmptyStateImageClassKey, MissingAnnotationEmptyStateClassKey, @@ -98,7 +98,7 @@ type BackstageComponentsNameToClassKey = { BackstageDependencyGraphDefaultNode: DependencyGraphDefaultNodeClassKey; BackstageDependencyGraphEdge: DependencyGraphEdgeClassKey; BackstageDependencyGraphNode: DependencyGraphNodeClassKey; - BackstageDismissableBanner: DismissbleBannerClassKey; + BackstageDismissableBanner: DismissableBannerClassKey; BackstageEmptyState: EmptyStateClassKey; BackstageEmptyStateImage: EmptyStateImageClassKey; BackstageMissingAnnotationEmptyState: MissingAnnotationEmptyStateClassKey; @@ -165,6 +165,7 @@ type BackstageComponentsNameToClassKey = { BackstageCardTab: CardTabClassKey; }; +/** @public */ export type BackstageOverrides = Overrides & { [Name in keyof BackstageComponentsNameToClassKey]?: Partial< StyleRules From 7a5cb137b599094a019904ad6053031829dfb18c Mon Sep 17 00:00:00 2001 From: Kenneth Feng Date: Fri, 22 Oct 2021 21:45:04 -0400 Subject: [PATCH 089/245] Always use base64 encoding to prevent interpreting binary files as utf-8 text files Signed-off-by: Kenneth Feng --- .../builtin/publish/githubPullRequest.test.ts | 16 ++++++++-------- .../actions/builtin/publish/githubPullRequest.ts | 14 +++++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index 1de9a7e268..27c6f1e59d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -99,8 +99,8 @@ describe('createPublishGithubPullRequestAction', () => { commit: 'Create my new app', files: { 'file.txt': { - encoding: 'utf-8', - content: 'Hello there!', + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', mode: '100644', }, }, @@ -172,8 +172,8 @@ describe('createPublishGithubPullRequestAction', () => { commit: 'Create my new app', files: { 'foo.txt': { - content: 'Hello there!', - encoding: 'utf-8', + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', mode: '100644', }, }, @@ -230,8 +230,8 @@ describe('createPublishGithubPullRequestAction', () => { commit: 'Create my new app', files: { 'file.txt': { - content: 'Hello there!', - encoding: 'utf-8', + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', mode: '100644', }, }, @@ -298,8 +298,8 @@ describe('createPublishGithubPullRequestAction', () => { commit: 'Create my new app', files: { 'file.txt': { - content: 'Hello there!', - encoding: 'utf-8', + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', mode: '100755', }, }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index aec59c86fd..21361072ef 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -196,15 +196,23 @@ export const createPublishGithubPullRequestAction = ({ const fileContents = await Promise.all( localFilePaths.map(filePath => { const absPath = path.resolve(fileRoot, filePath); - const content = fs.readFileSync(absPath).toString(); + const base64EncodedContent = fs + .readFileSync(absPath) + .toString('base64'); const fileStat = fs.statSync(absPath); const isExecutable = fileStat.mode === 33277; // aka. 100755 // See the properties of tree items // in https://docs.github.com/en/rest/reference/git#trees const githubTreeItemMode = isExecutable ? '100755' : '100644'; + // Always use base64 encoding to avoid doubling a binary file in size + // due to interpreting a binary file as utf-8 and sending github + // the utf-8 encoded content. + // + // For example, the original gradle-wrapper.jar is 57.8k in https://github.com/kennethzfeng/pull-request-test/pull/5/files. + // Its size could be doubled to 98.3K (See https://github.com/kennethzfeng/pull-request-test/pull/4/files) return { - encoding: 'utf-8', - content: content, + encoding: 'base64', + content: base64EncodedContent, mode: githubTreeItemMode, }; }), From 75a865215cd70a1bd41e136d56ea5240b749e464 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 23 Oct 2021 11:47:54 -0500 Subject: [PATCH 090/245] Added duration and identity as well as minor refactoring Signed-off-by: Andre Wanlin --- plugins/azure-devops-backend/api-report.md | 3 + .../src/api/AzureDevOpsApi.test.ts | 70 +++++++++++++++++-- .../src/api/AzureDevOpsApi.ts | 15 ++-- plugins/azure-devops-backend/src/api/types.ts | 3 + 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 4b49872161..fac6261a39 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -81,7 +81,10 @@ export type RepoBuild = { status?: BuildStatus; result?: BuildResult; queueTime?: Date; + startTime?: Date; + finishTime?: Date; source: string; + uniqueName?: string; }; // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index fd9aa169d5..fdd49c0440 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi'; -import { PullRequest, RepoBuild } from './types'; + import { Build, BuildResult, BuildStatus, - DefinitionReference, -} from 'azure-devops-node-api/interfaces/BuildInterfaces'; -import { GitPullRequest, + GitRepository, + PullRequest, PullRequestStatus, -} from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { GitRepository } from 'azure-devops-node-api/interfaces/TfvcInterfaces'; + RepoBuild, +} from './types'; +import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi'; + +import { DefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; describe('AzureDevOpsApi', () => { @@ -41,16 +42,24 @@ describe('AzureDevOpsApi', () => { }, }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAINjdoe', + }; + const inputBuild: Build = { id: 1, buildNumber: 'Build-1', status: BuildStatus.Completed, result: BuildResult.Succeeded, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: inputBuildDefinition, _links: inputLinks, + requestedFor: inputIdentityRef, }; const outputRepoBuild: RepoBuild = { @@ -60,7 +69,10 @@ describe('AzureDevOpsApi', () => { status: BuildStatus.Completed, result: BuildResult.Succeeded, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAINjdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -75,16 +87,24 @@ describe('AzureDevOpsApi', () => { }, }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAINjdoe', + }; + const inputBuild: Build = { id: 1, buildNumber: 'Build-1', status: BuildStatus.Completed, result: BuildResult.Succeeded, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, _links: inputLinks, + requestedFor: inputIdentityRef, }; const outputRepoBuild: RepoBuild = { @@ -94,7 +114,10 @@ describe('AzureDevOpsApi', () => { status: BuildStatus.Completed, result: BuildResult.Succeeded, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAINjdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -109,16 +132,24 @@ describe('AzureDevOpsApi', () => { }, }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAINjdoe', + }; + const inputBuild: Build = { id: 1, buildNumber: 'Build-1', status: undefined, result: BuildResult.Succeeded, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, _links: inputLinks, + requestedFor: inputIdentityRef, }; const outputRepoBuild: RepoBuild = { @@ -128,7 +159,10 @@ describe('AzureDevOpsApi', () => { status: BuildStatus.None, result: BuildResult.Succeeded, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAINjdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -143,16 +177,24 @@ describe('AzureDevOpsApi', () => { }, }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAINjdoe', + }; + const inputBuild: Build = { id: 1, buildNumber: 'Build-1', status: BuildStatus.InProgress, result: undefined, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, _links: inputLinks, + requestedFor: inputIdentityRef, }; const outputRepoBuild: RepoBuild = { @@ -162,7 +204,10 @@ describe('AzureDevOpsApi', () => { status: BuildStatus.InProgress, result: BuildResult.None, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAINjdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); @@ -171,16 +216,24 @@ describe('AzureDevOpsApi', () => { describe('mappedRepoBuild with undefined link', () => { it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAINjdoe', + }; + const inputBuild: Build = { id: 1, buildNumber: 'Build-1', status: BuildStatus.InProgress, result: undefined, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), sourceBranch: 'refs/heads/develop', sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', definition: undefined, _links: undefined, + requestedFor: inputIdentityRef, }; const outputRepoBuild: RepoBuild = { @@ -190,7 +243,10 @@ describe('AzureDevOpsApi', () => { status: BuildStatus.InProgress, result: BuildResult.None, queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + startTime: new Date('2020-09-12T06:15:23.9325232Z'), + finishTime: new Date('2020-09-12T06:20:23.9325232Z'), source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAINjdoe', }; expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 1a80a3d42b..26378496ea 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -111,7 +111,7 @@ export class AzureDevOpsApi { options: PullRequestOptions, ): Promise { this.logger?.debug( - `Calling Azure DevOps REST API, getting up to ${top} Pull Requests for Repository ${repoName} for Project ${projectName}`, + `Calling Azure DevOps REST API, getting up to ${options.top} Pull Requests for Repository ${repoName} for Project ${projectName}`, ); const gitRepository = await this.getGitRepository(projectName, repoName); @@ -144,11 +144,14 @@ export function mappedRepoBuild(build: Build): RepoBuild { title: [build.definition?.name, build.buildNumber] .filter(Boolean) .join(' - '), - link: build._links?.web.href ? build._links?.web.href : '', - status: build.status ? build.status : BuildStatus.None, - result: build.result ? build.result : BuildResult.None, + link: build._links?.web.href ?? '', + status: build.status ?? BuildStatus.None, + result: build.result ?? BuildResult.None, queueTime: build.queueTime, + startTime: build.startTime, + finishTime: build.finishTime, source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, + uniqueName: build.requestedFor?.uniqueName ?? 'N/A', }; } @@ -160,8 +163,8 @@ export function mappedPullRequest( pullRequestId: pullRequest.pullRequestId, repoName: pullRequest.repository?.name, title: pullRequest.title, - uniqueName: pullRequest.createdBy?.uniqueName, - createdBy: pullRequest.createdBy?.displayName, + uniqueName: pullRequest.createdBy?.uniqueName ?? 'N/A', + createdBy: pullRequest.createdBy?.displayName ?? 'N/A', creationDate: pullRequest.creationDate, sourceRefName: pullRequest.sourceRefName, targetRefName: pullRequest.targetRefName, diff --git a/plugins/azure-devops-backend/src/api/types.ts b/plugins/azure-devops-backend/src/api/types.ts index 0fd30243af..fab61d26c9 100644 --- a/plugins/azure-devops-backend/src/api/types.ts +++ b/plugins/azure-devops-backend/src/api/types.ts @@ -41,7 +41,10 @@ export type RepoBuild = { status?: BuildStatus; result?: BuildResult; queueTime?: Date; + startTime?: Date; + finishTime?: Date; source: string; + uniqueName?: string; }; export type PullRequest = { From 2eebc9bac3d71ec387cf352b3b679fb0961c29af Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 23 Oct 2021 11:50:43 -0500 Subject: [PATCH 091/245] Added changeset Signed-off-by: Andre Wanlin --- .changeset/kind-islands-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/kind-islands-attack.md diff --git a/.changeset/kind-islands-attack.md b/.changeset/kind-islands-attack.md new file mode 100644 index 0000000000..a549839bc7 --- /dev/null +++ b/.changeset/kind-islands-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +--- + +Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641 From e1ebaff47a1f6e73437495eb1d6da6d015431012 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 24 Oct 2021 08:09:00 -0500 Subject: [PATCH 092/245] Removed router Signed-off-by: Andre Wanlin --- .../azure-devops/src/components/Router.tsx | 43 ------------------- plugins/azure-devops/src/index.ts | 7 ++- plugins/azure-devops/src/plugin.ts | 15 +++++-- 3 files changed, 17 insertions(+), 48 deletions(-) delete mode 100644 plugins/azure-devops/src/components/Router.tsx diff --git a/plugins/azure-devops/src/components/Router.tsx b/plugins/azure-devops/src/components/Router.tsx deleted file mode 100644 index 3000deb5cc..0000000000 --- a/plugins/azure-devops/src/components/Router.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { Routes, Route } from 'react-router'; -import { azureDevOpsRouteRef } from '../routes'; -import { EntityPageAzurePipelines } from './EntityPageAzurePipelines'; -import { AZURE_DEVOPS_ANNOTATION } from '../constants'; -import { Entity } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { MissingAnnotationEmptyState } from '@backstage/core-components'; - -export const isAzureDevOpsAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]); - -export const Router = ({ defaultLimit }: { defaultLimit?: number }) => { - const { entity } = useEntity(); - - if (!isAzureDevOpsAvailable(entity)) { - return ; - } - - return ( - - } - /> - - ); -}; diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts index 9a90d6ed6a..71d3b654fe 100644 --- a/plugins/azure-devops/src/index.ts +++ b/plugins/azure-devops/src/index.ts @@ -13,5 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { azureDevOpsPlugin, EntityAzurePipelinesContent } from './plugin'; -export { isAzureDevOpsAvailable } from './components/Router'; +export { + azureDevOpsPlugin, + EntityAzurePipelinesContent, + isAzureDevOpsAvailable, +} from './plugin'; diff --git a/plugins/azure-devops/src/plugin.ts b/plugins/azure-devops/src/plugin.ts index 1cf6e680d0..ab6d816460 100644 --- a/plugins/azure-devops/src/plugin.ts +++ b/plugins/azure-devops/src/plugin.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { azureDevOpsApiRef } from './api/AzureDevOpsApi'; -import { AzureDevOpsClient } from './api/AzureDevOpsClient'; import { createApiFactory, createPlugin, @@ -23,8 +21,16 @@ import { discoveryApiRef, identityApiRef, } from '@backstage/core-plugin-api'; + +import { AZURE_DEVOPS_ANNOTATION } from './constants'; +import { AzureDevOpsClient } from './api/AzureDevOpsClient'; +import { Entity } from '@backstage/catalog-model'; +import { azureDevOpsApiRef } from './api/AzureDevOpsApi'; import { azureDevOpsRouteRef } from './routes'; +export const isAzureDevOpsAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]); + export const azureDevOpsPlugin = createPlugin({ id: 'azureDevOps', apis: [ @@ -43,7 +49,10 @@ export const azureDevOpsPlugin = createPlugin({ export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide( createRoutableExtension({ name: 'EntityAzurePipelinesContent', - component: () => import('./components/Router').then(m => m.Router), + component: () => + import('./components/EntityPageAzurePipelines').then( + m => m.EntityPageAzurePipelines, + ), mountPoint: azureDevOpsRouteRef, }), ); From 7adcfb4f764a3dc066f6f84cca3528fe4c40f8b6 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 24 Oct 2021 08:45:22 -0500 Subject: [PATCH 093/245] Refactored README Signed-off-by: Andre Wanlin --- plugins/azure-devops/README.md | 116 +++++++++------------------------ 1 file changed, 32 insertions(+), 84 deletions(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index edf26ecfb3..47a0c4debd 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -2,74 +2,50 @@ Website: [https://dev.azure.com/](https://dev.azure.com/) -![Azure DevOps Builds Example](./docs/azure-devops-builds.png) +## Features + +### Azure Pipelines + +Lists the top _n_ builds for a given repository where _n_ is a configurable value + +![Azure Pipelines Builds Example](./docs/azure-devops-builds.png) ## Setup The following sections will help you get the Azure DevOps plugin setup and running -### Configuration +### Azure DevOps Backend -The Azure DevOps plugin requires the following YAML to be added to your app-config.yaml: +You need to setup the [Azure DevOps backend plugin](https://github.com/backstage/backstage/tree/master/plugins/azure-devops-backend) before you move forward with any of these steps if you haven't already + +### Entity Annotation + +To be able to use the Azure DevOps plugin you need to add the following annotation to any entities you want to use it with: ```yaml -azureDevOps: - host: dev.azure.com - token: ${AZURE_TOKEN} - organization: my-company +dev.azure.com/project-repo: / ``` -Configuration Details: +Let's break this down a little: `` will be the name of your Team Project and `` will be the name of your repository which needs to be part of the Team Project you entered for ``. -- `host` and `token` can be the same as the ones used for the `integration` section -- `AZURE_TOKEN` environment variable must be set to a [Personal Access Token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) with read access to both Code and Build -- `organization` is your Azure DevOps Organization name or for Azure DevOps Server (on-premise) this will be your Collection name +Here's what that will look like in action: -### Backend +```yaml +# Example catalog-info.yaml entity definition file +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + dev.azure.com/project-repo: my-project/my-repo +spec: + type: service + # ... +``` -Here's how to get the backend up and running: +### Azure Pipelines Component -1. First we need to add the `@backstage/plugin-azure-devops-backend` package to your backend: - - ```sh - # From the Backstage root directory - cd packages/backend - yarn add @backstage/plugin-azure-devops-backend - ``` - -2. Then we will create a new file named `packages/backend/src/plugins/azure-devops.ts`, and add the - following to it: - - ```ts - import { createRouter } from '@backstage/plugin-azure-devops-backend'; - import { Router } from 'express'; - import type { PluginEnvironment } from '../types'; - - export default function createPlugin({ - logger, - config, - }: PluginEnvironment): Promise { - return createRouter({ logger, config }); - } - ``` - -3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: - - ```ts - import azureDevOps from './plugins/azuredevops'; - // ... - async function main() { - // ... - const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops')); - apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv)); - ``` - -4. Now run `yarn start-backend` from the repo root -5. Finally open `http://localhost:7000/api/azure-devops/health` in a browser and it should return `{"status":"ok"}` - -### Frontend - -To get the frontend working you'll need to do the following two steps: +To get the Azure Pipelines component working you'll need to do the following two steps: 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: @@ -101,35 +77,7 @@ To get the frontend working you'll need to do the following two steps: ``` -### Entity Annotation - -You need to add the following annotation to any entities you want to be able to use the Azure Devops plugin with: - -```yaml -dev.azure.com/project-repo: / -``` - -Let's break this down a little: `` will be the name of your Team Project and `` will be the name of your repository which needs to be part of the Team Project you entered for ``. - -Here's what that will look like in action: - -```yaml -# Example catalog-info.yaml entity definition file -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - # ... - annotations: - dev.azure.com/project-repo: my-project/my-repo -spec: - type: service - # ... -``` - -## Features - -- Lists the top _n_ builds for a given repository where _n_ is the value configured for `top` - ## Limitations -- Currently multiple organizations is not supported +- Currently multiple organizations are not supported +- Mixing Azure DevOps Services (cloud) and Azure DevOps Server (on-premise) is not supported From 91e7f2eda8c8d168109c51b6643d4281940e4c29 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 24 Oct 2021 10:04:11 -0500 Subject: [PATCH 094/245] Added tests for state and result functions Signed-off-by: Andre Wanlin --- .../components/BuildTable/BuildTable.test.ts | 241 ++++++++++++++++++ .../src/components/BuildTable/BuildTable.tsx | 4 +- 2 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts b/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts new file mode 100644 index 0000000000..42408f37b3 --- /dev/null +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts @@ -0,0 +1,241 @@ +/* + * 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 { + BuildResult, + BuildStatus, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { getBuildResultComponent, getBuildStateComponent } from './BuildTable'; + +import { renderInTestApp } from '@backstage/test-utils'; + +describe('getBuildResultComponent', () => { + describe('getBuildResultComponent with Succeeded result', () => { + it('should return Status ok Succeeded', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildResultComponent(BuildResult.Succeeded), + ); + + expect(getByLabelText('Status ok')).toBeInTheDocument(); + expect(getByText('Succeeded')).toBeInTheDocument(); + }); + }); + + describe('getBuildResultComponent with Partially Succeeded result', () => { + it('should return Status warning Partially Succeeded', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildResultComponent(BuildResult.PartiallySucceeded), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Partially Succeeded')).toBeInTheDocument(); + }); + }); + + describe('getBuildResultComponent with Failed result', () => { + it('should return Status error Failed', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildResultComponent(BuildResult.Failed), + ); + + expect(getByLabelText('Status error')).toBeInTheDocument(); + expect(getByText('Failed')).toBeInTheDocument(); + }); + }); + + describe('getBuildResultComponent with Canceled result', () => { + it('should return Status aborted Canceled', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildResultComponent(BuildResult.Canceled), + ); + + expect(getByLabelText('Status aborted')).toBeInTheDocument(); + expect(getByText('Canceled')).toBeInTheDocument(); + }); + }); + + describe('getBuildResultComponent with None result', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildResultComponent(BuildResult.None), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); + + describe('getBuildResultComponent with undefined result', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildResultComponent(undefined), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); +}); + +describe('getBuildStateComponent', () => { + describe('getBuildStateComponent with In Progress status and undefined result', () => { + it('should return Status running In Progress', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.InProgress, undefined), + ); + + expect(getByLabelText('Status running')).toBeInTheDocument(); + expect(getByText('In Progress')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Completed status and Succeeded result', () => { + it('should return Status ok Succeeded', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Completed, BuildResult.Succeeded), + ); + + expect(getByLabelText('Status ok')).toBeInTheDocument(); + expect(getByText('Succeeded')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Completed status and Partially Succeeded result', () => { + it('should return Status warning Partially Succeeded', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent( + BuildStatus.Completed, + BuildResult.PartiallySucceeded, + ), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Partially Succeeded')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Completed status and Failed result', () => { + it('should return Status error Failed', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Completed, BuildResult.Failed), + ); + + expect(getByLabelText('Status error')).toBeInTheDocument(); + expect(getByText('Failed')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Completed status and Canceled result', () => { + it('should return Status aborted Canceled', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Completed, BuildResult.Canceled), + ); + + expect(getByLabelText('Status aborted')).toBeInTheDocument(); + expect(getByText('Canceled')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Completed status and None result', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Completed, BuildResult.None), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Completed status and undefined result', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Completed, undefined), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); + + // TODO: Add remaining Completed iterations + + describe('getBuildStateComponent with Cancelling status and undefined result', () => { + it('should return Status aborted Cancelling', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Cancelling, undefined), + ); + + expect(getByLabelText('Status aborted')).toBeInTheDocument(); + expect(getByText('Cancelling')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Postponed status and undefined result', () => { + it('should return Status pending Postponed', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.Postponed, undefined), + ); + + expect(getByLabelText('Status pending')).toBeInTheDocument(); + expect(getByText('Postponed')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with Not Started status and undefined result', () => { + it('should return Status aborted Not Started', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.NotStarted, undefined), + ); + + expect(getByLabelText('Status aborted')).toBeInTheDocument(); + expect(getByText('Not Started')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with None status and undefined result', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(BuildStatus.None, undefined), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with undefined and undefined result', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(undefined, undefined), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); + + describe('getBuildStateComponent with undefined and any valid BuildResult', () => { + it('should return Status warning Unknown', async () => { + const { getByLabelText, getByText } = await renderInTestApp( + getBuildStateComponent(undefined, BuildResult.Succeeded), + ); + + expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('Unknown')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index 7516ae52a4..a308afb81b 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -36,7 +36,7 @@ import { DateTime } from 'luxon'; import React from 'react'; import { RepoBuild } from '../../api/types'; -const getBuildResultComponent = (result: number | undefined) => { +export const getBuildResultComponent = (result: number | undefined) => { switch (result) { case BuildResult.Succeeded: return ( @@ -72,7 +72,7 @@ const getBuildResultComponent = (result: number | undefined) => { } }; -const getBuildStateComponent = ( +export const getBuildStateComponent = ( status: number | undefined, result: number | undefined, ) => { From 7359623e8775a58479e79717a4cc470497f3cbd4 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 24 Oct 2021 10:13:28 -0500 Subject: [PATCH 095/245] Added changeset Signed-off-by: Andre Wanlin --- .changeset/green-parrots-thank.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/green-parrots-thank.md diff --git a/.changeset/green-parrots-thank.md b/.changeset/green-parrots-thank.md new file mode 100644 index 0000000000..9296a5119a --- /dev/null +++ b/.changeset/green-parrots-thank.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-azure-devops': patch +--- + +Azure DevOps frontend refactoring items from issue #7641 + +- Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions +- Improved documentation to be easier to expand with new features in the future +- Removed Router based on feedback from maintainers +- Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable From 3c4bbd7ad6f8ece322f607a8acfc56fcc4af1d96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 04:12:29 +0000 Subject: [PATCH 096/245] build(deps): bump keyv-memcache from 1.2.5 to 1.2.7 Bumps [keyv-memcache](https://github.com/jaredwray/keyv-memcache) from 1.2.5 to 1.2.7. - [Release notes](https://github.com/jaredwray/keyv-memcache/releases) - [Commits](https://github.com/jaredwray/keyv-memcache/compare/v1.2.5...v1.2.7) --- updated-dependencies: - dependency-name: keyv-memcache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index c905fbea6f..3f2d90b77a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7604,7 +7604,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.9": +"@types/react@*", "@types/react@>=16.9.0": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== @@ -7613,6 +7613,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^16.9": + version "16.14.19" + resolved "https://registry.npmjs.org/@types/react/-/react-16.14.19.tgz#e818de0026795b621c41a271d74b73e108b737bf" + integrity sha512-NHSXGpkJLcP5slugb/l0PhXoPRbHPvTpMraP9n8WS4mTP4cr/Y9QfK9reGq5WFKGYV73dM1uN4eDpqrr74fyQg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/recharts@^1.8.14", "@types/recharts@^1.8.15": version "1.8.19" resolved "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.19.tgz#047f72cf4c25df545aa1085fe3a085e58a2483c1" @@ -18400,9 +18409,9 @@ keytar@^7.3.0: prebuild-install "^6.0.0" keyv-memcache@^1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/keyv-memcache/-/keyv-memcache-1.2.5.tgz#9097af5c617dc740e7300ebfd4a2efb8917429e3" - integrity sha512-iG+GHlhXyV83gmlCtMFIqNYVvv0i0towAy42NvziUR7D+k9jp81fAq0lu66H4gooOnW+ojkdigRvRpL40j1f7w== + version "1.2.7" + resolved "https://registry.npmjs.org/keyv-memcache/-/keyv-memcache-1.2.7.tgz#b8a43eeecdb11ad8f4d6d64abd4298d014c74955" + integrity sha512-lD7QaHf9M+bq9XLFZVEXJ2HNqEvO5CmtjiJjB2FHifbEQ18+YRDTfJCjo88Roc0hXtIqz/QZjYXpIYL8EWpL1g== dependencies: json-buffer "^3.0.1" memjs "^1.3.0" @@ -20351,7 +20360,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" From d184e50c50937f00e971b2513e35f64273d18a14 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 25 Oct 2021 10:21:17 +0200 Subject: [PATCH 097/245] chore: updating api report Signed-off-by: blam --- plugins/catalog-react/api-report.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index e255ea9974..77fa821d36 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -649,10 +649,9 @@ export class EntityTypeFilter implements EntityFilter { // Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityTypePicker: ({ - hidden, - initialFilter, -}: EntityTypeFilterProps) => JSX.Element | null; +export const EntityTypePicker: ( + props: EntityTypeFilterProps, +) => JSX.Element | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts From 4289ac5322f5316655360352d70f845a5ff6fc13 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 25 Oct 2021 10:34:01 +0200 Subject: [PATCH 098/245] chore: export the types properly and update the api-report Signed-off-by: blam --- plugins/catalog-react/api-report.md | 9 ++++++++- .../src/components/EntityTypePicker/EntityTypePicker.tsx | 2 +- .../src/components/EntityTypePicker/index.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 77fa821d36..361635c891 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -645,7 +645,14 @@ export class EntityTypeFilter implements EntityFilter { readonly value: string | string[]; } -// Warning: (ae-forgotten-export) The symbol "EntityTypeFilterProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EntityTypeFilterProps = { + initialFilter?: string; + hidden?: boolean; +}; + // Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index 23451ebcfe..225193dc93 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,7 +22,7 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -type EntityTypeFilterProps = { +export type EntityTypeFilterProps = { initialFilter?: string; hidden?: boolean; }; diff --git a/plugins/catalog-react/src/components/EntityTypePicker/index.ts b/plugins/catalog-react/src/components/EntityTypePicker/index.ts index 6e888516f3..1124b5c30c 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/index.ts +++ b/plugins/catalog-react/src/components/EntityTypePicker/index.ts @@ -15,3 +15,4 @@ */ export { EntityTypePicker } from './EntityTypePicker'; +export type { EntityTypeFilterProps } from './EntityTypePicker'; From 43b77e1e5e855335da997f74562c9fd13b924697 Mon Sep 17 00:00:00 2001 From: rodion Date: Mon, 25 Oct 2021 15:05:07 +0300 Subject: [PATCH 099/245] fix: sentry-plugin query parameter is optional Signed-off-by: rodion --- plugins/sentry/api-report.md | 4 ++-- plugins/sentry/src/api/production-api.ts | 2 +- plugins/sentry/src/api/sentry-api.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index 89c1dd1bca..078e89d96d 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -39,7 +39,7 @@ export class ProductionSentryApi implements SentryApi { fetchIssues( project: string, statsFor: string, - query: string, + query?: string, ): Promise; } @@ -56,7 +56,7 @@ export interface SentryApi { fetchIssues( project: string, statsFor: string, - query: string, + query?: string, ): Promise; } diff --git a/plugins/sentry/src/api/production-api.ts b/plugins/sentry/src/api/production-api.ts index d9ece065d3..5968d20011 100644 --- a/plugins/sentry/src/api/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -27,7 +27,7 @@ export class ProductionSentryApi implements SentryApi { async fetchIssues( project: string, statsFor: string, - query: string, + query?: string, ): Promise { if (!project) { return []; diff --git a/plugins/sentry/src/api/sentry-api.ts b/plugins/sentry/src/api/sentry-api.ts index 5c0fb318a6..d2177cde2a 100644 --- a/plugins/sentry/src/api/sentry-api.ts +++ b/plugins/sentry/src/api/sentry-api.ts @@ -26,6 +26,6 @@ export interface SentryApi { fetchIssues( project: string, statsFor: string, - query: string, + query?: string, ): Promise; } From 1695c17b2e45a42288b695ef704c5e5e7a56096a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 21 Oct 2021 15:18:18 +0200 Subject: [PATCH 100/245] storybook: support args for selecting a subset of packages Signed-off-by: Patrik Oldsberg --- packages/storybook/.storybook/main.js | 120 +++++++++++++++----------- packages/storybook/README.md | 10 +++ 2 files changed, 78 insertions(+), 52 deletions(-) diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 4afcac335c..5bdc6b3a2f 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -1,61 +1,77 @@ const path = require('path'); const WebpackPluginFailBuildOnWarning = require('./webpack-plugin-fail-build-on-warning'); -module.exports = { - stories: [ - '../../core-components/src/**/*.stories.tsx', - '../../../plugins/**/src/**/*.stories.tsx', - ], - addons: [ - '@storybook/addon-a11y', - '@storybook/addon-actions', - '@storybook/addon-links', - '@storybook/addon-storysource', - 'storybook-dark-mode/register', - ], - webpackFinal: async config => { - // Mirror config in packages/cli/src/lib/bundler - config.resolve.mainFields = ['browser', 'module', 'main']; +module.exports = ({ args }) => { + // Calling storybook with no args causes our default list of stories to be used. + // This set of stories are the ones that we publish to backstage.io + // + // If it's called with args, each arg should be the path to a package that we will + // show the stories from, for example `yarn storybook plugins/catalog`. + let stories; + if (args.length === 0) { + stories = [ + '../../core-components/src/**/*.stories.tsx', + '../../../plugins/org/src/**/*.stories.tsx', + '../../../plugins/search/src/**/*.stories.tsx', + ]; + } else { + const rootDir = path.resolve(__dirname, '../../..'); + stories = args.map(arg => path.join(rootDir, arg, 'src/**/*.stories.tsx')); + } - // Remove the default babel-loader for js files, we're using sucrase instead - const [jsLoader] = config.module.rules.splice(0, 1); - if (!jsLoader.use[0].loader.includes('babel-loader')) { - throw new Error( - `Unexpected loader removed from storybook config, ${jsLoader.use[0].loader}`, + return { + stories, + addons: [ + '@storybook/addon-a11y', + '@storybook/addon-actions', + '@storybook/addon-links', + '@storybook/addon-storysource', + 'storybook-dark-mode/register', + ], + webpackFinal: async config => { + // Mirror config in packages/cli/src/lib/bundler + config.resolve.mainFields = ['browser', 'module', 'main']; + + // Remove the default babel-loader for js files, we're using sucrase instead + const [jsLoader] = config.module.rules.splice(0, 1); + if (!jsLoader.use[0].loader.includes('babel-loader')) { + throw new Error( + `Unexpected loader removed from storybook config, ${jsLoader.use[0].loader}`, + ); + } + + config.resolve.extensions.push('.ts', '.tsx'); + + config.module.rules.push( + { + test: /\.(tsx?)$/, + exclude: /node_modules/, + loader: require.resolve('@sucrase/webpack-loader'), + options: { + transforms: ['typescript', 'jsx', 'react-hot-loader'], + }, + }, + { + test: /\.(jsx?|mjs)$/, + exclude: /node_modules/, + loader: require.resolve('@sucrase/webpack-loader'), + options: { + transforms: ['jsx', 'react-hot-loader'], + }, + }, ); - } - config.resolve.extensions.push('.ts', '.tsx'); + // Disable ProgressPlugin which logs verbose webpack build progress. Warnings and Errors are still logged. + config.plugins = config.plugins.filter( + ({ constructor }) => constructor.name !== 'ProgressPlugin', + ); - config.module.rules.push( - { - test: /\.(tsx?)$/, - exclude: /node_modules/, - loader: require.resolve('@sucrase/webpack-loader'), - options: { - transforms: ['typescript', 'jsx', 'react-hot-loader'], - }, - }, - { - test: /\.(jsx?|mjs)$/, - exclude: /node_modules/, - loader: require.resolve('@sucrase/webpack-loader'), - options: { - transforms: ['jsx', 'react-hot-loader'], - }, - }, - ); + // Fail storybook build on CI if there are webpack warnings. + if (process.env.CI) { + config.plugins.push(new WebpackPluginFailBuildOnWarning()); + } - // Disable ProgressPlugin which logs verbose webpack build progress. Warnings and Errors are still logged. - config.plugins = config.plugins.filter( - ({ constructor }) => constructor.name !== 'ProgressPlugin', - ); - - // Fail storybook build on CI if there are webpack warnings. - if (process.env.CI) { - config.plugins.push(new WebpackPluginFailBuildOnWarning()); - } - - return config; - }, + return config; + }, + }; }; diff --git a/packages/storybook/README.md b/packages/storybook/README.md index f966a358d3..600e2409ab 100644 --- a/packages/storybook/README.md +++ b/packages/storybook/README.md @@ -2,6 +2,16 @@ This package provides a Storybook build for Backstage. See https://backstage.io/storybook/. +## Usage + +To run the storybook locally, call `yarn storybook` in repo root. This will show the default set of stories that we publish to https://backstage.io/storybook + +If you want to show stories other than the default set, you can pass one or more package paths as an argument when calling storybook, for example: + +```sh +yarn storybook plugins/search +``` + ## Why is this not part of `@backstage/core-components`? This separate storybook package exists because of dependency conflicts with `@backstage/cli`. It uses `nohoist` to avoid the conflicts, and since you can only use that in private packages it has to be separated out of `@backstage/core-components`. From e571311f8ade8607736a4feda7f05841cad7ffc4 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Mon, 25 Oct 2021 09:11:22 -0400 Subject: [PATCH 101/245] downgrade changeset impact to patch for bucketRootPath implementation Signed-off-by: Colton Padden --- .changeset/quick-walls-know.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/quick-walls-know.md b/.changeset/quick-walls-know.md index 6e7d7faa03..14dd78364f 100644 --- a/.changeset/quick-walls-know.md +++ b/.changeset/quick-walls-know.md @@ -1,5 +1,5 @@ --- -'@backstage/techdocs-common': minor +'@backstage/techdocs-common': patch --- -Support optional bucketRootPath in S3 and GCS publishers +Support optional bucketRootPath configuration parameter in S3 and GCS publishers From 5e10974af6c614df6f4828c5c97a2846d340e081 Mon Sep 17 00:00:00 2001 From: jrusso1020 Date: Sun, 24 Oct 2021 13:28:15 -0400 Subject: [PATCH 102/245] Export components so developers can customize the scaffolder page This is the first step to allow developers to customize the scaffolder page. With this work all the scaffolder page is now available to backstage developers and they can customize the current scaffolder page how they like similar to what is available for the catalog page and other plugin pages. Future work that has been discussed is possibly allowing for SwimLane components that will allow developers to surface specific templates or organize templates in specific ways Signed-off-by: jrusso1020 --- .changeset/friendly-olives-drop.md | 5 ++++ plugins/scaffolder/api-report.md | 23 +++++++++++++++++++ .../src/components/TemplateList/index.ts | 1 + plugins/scaffolder/src/index.ts | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 .changeset/friendly-olives-drop.md diff --git a/.changeset/friendly-olives-drop.md b/.changeset/friendly-olives-drop.md new file mode 100644 index 0000000000..371cf0e4db --- /dev/null +++ b/.changeset/friendly-olives-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 740685e922..554fccabb7 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -240,6 +240,29 @@ const scaffolderPlugin: BackstagePlugin< export { scaffolderPlugin as plugin }; export { scaffolderPlugin }; +// Warning: (ae-missing-release-tag) "TemplateList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TemplateList: ({ + TemplateCardComponent, +}: TemplateListProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "TemplateListProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TemplateListProps = { + TemplateCardComponent?: + | ComponentType<{ + template: TemplateEntityV1beta2; + }> + | undefined; +}; + +// Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TemplateTypePicker: () => JSX.Element | null; + // Warning: (ae-missing-release-tag) "TextValuePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/scaffolder/src/components/TemplateList/index.ts b/plugins/scaffolder/src/components/TemplateList/index.ts index b9ec700d74..0b62f9cbb1 100644 --- a/plugins/scaffolder/src/components/TemplateList/index.ts +++ b/plugins/scaffolder/src/components/TemplateList/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { TemplateList } from './TemplateList'; +export type { TemplateListProps } from './TemplateList'; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 4de701bf7a..0180c24b90 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -44,3 +44,6 @@ export { TextValuePicker, } from './components/fields'; export { FavouriteTemplate } from './components/FavouriteTemplate'; +export { TemplateList } from './components/TemplateList'; +export type { TemplateListProps } from './components/TemplateList'; +export { TemplateTypePicker } from './components/TemplateTypePicker'; From ce024e93ce042faedd4ff238c8bb111fa4107166 Mon Sep 17 00:00:00 2001 From: Kenneth Feng Date: Mon, 25 Oct 2021 10:16:44 -0400 Subject: [PATCH 103/245] Cast string 'base64' to the compatible Encoding type. Signed-off-by: Kenneth Feng --- .../scaffolder/actions/builtin/publish/githubPullRequest.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 21361072ef..e147f49b23 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -30,6 +30,8 @@ import { createPullRequest } from 'octokit-plugin-create-pull-request'; import globby from 'globby'; import { resolveSafeChildPath } from '@backstage/backend-common'; +export type Encoding = 'utf-8' | 'base64'; + class GithubResponseError extends CustomErrorBase {} type CreatePullRequestResponse = { @@ -210,8 +212,9 @@ export const createPublishGithubPullRequestAction = ({ // // For example, the original gradle-wrapper.jar is 57.8k in https://github.com/kennethzfeng/pull-request-test/pull/5/files. // Its size could be doubled to 98.3K (See https://github.com/kennethzfeng/pull-request-test/pull/4/files) + const encoding: Encoding = 'base64'; return { - encoding: 'base64', + encoding: encoding, content: base64EncodedContent, mode: githubTreeItemMode, }; From 004a23b5fcb88a645a7ad0c06cbba53b9cd96c4b Mon Sep 17 00:00:00 2001 From: Kenneth Feng Date: Mon, 25 Oct 2021 10:18:56 -0400 Subject: [PATCH 104/245] update octokit-plugin-create-pull-request in yarn.lock Signed-off-by: Kenneth Feng --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index c905fbea6f..2b0af3e7fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21340,10 +21340,10 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -octokit-plugin-create-pull-request@^3.9.3: - version "3.9.3" - resolved "https://registry.npmjs.org/octokit-plugin-create-pull-request/-/octokit-plugin-create-pull-request-3.9.3.tgz#f99f53907ac322a3494cc970514a023d7b659e2b" - integrity sha512-lTyNnCRoT4IvCQx2Cb4eFMqg8aIpsaDd59MNwf4OPnWAJM7hT6g7RW/icImvAzZLR4t5ENSLNzWarv2XqLL+Lg== +octokit-plugin-create-pull-request@^3.10.0: + version "3.10.0" + resolved "https://registry.npmjs.org/octokit-plugin-create-pull-request/-/octokit-plugin-create-pull-request-3.10.0.tgz#c9a589e0014e949eadd24a03ace10565007784d5" + integrity sha512-QU3nk62+OimV7ki+pV90cXoqqbUAQLdbqccS7/cNajdjQ2KYmaakz21FL1y78a5N0mA2P4WOs0o2+aunvbWI0w== dependencies: "@octokit/types" "^6.8.2" From 5f3460089e12f2c5e7bc05f4dc5369f910bce378 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 25 Oct 2021 17:41:36 +0200 Subject: [PATCH 105/245] chore: revert yarn.lock changes for @types/react packages Signed-off-by: blam --- yarn.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3f2d90b77a..65d2d9cb2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7604,7 +7604,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0": +"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.9": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== @@ -7613,15 +7613,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^16.9": - version "16.14.19" - resolved "https://registry.npmjs.org/@types/react/-/react-16.14.19.tgz#e818de0026795b621c41a271d74b73e108b737bf" - integrity sha512-NHSXGpkJLcP5slugb/l0PhXoPRbHPvTpMraP9n8WS4mTP4cr/Y9QfK9reGq5WFKGYV73dM1uN4eDpqrr74fyQg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/recharts@^1.8.14", "@types/recharts@^1.8.15": version "1.8.19" resolved "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.19.tgz#047f72cf4c25df545aa1085fe3a085e58a2483c1" From 3e61a4da9e6849773319f4acf8e745c7eb3cd0dc Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 13 Sep 2021 12:21:29 +0200 Subject: [PATCH 106/245] backend: add some todos for redacting secrets from logs Co-authored-by: Harry Hogg Signed-off-by: Himanshu Mishra --- packages/backend-common/src/config.ts | 3 ++- .../backend-common/src/logging/rootLogger.ts | 26 +++++++++++++++++++ packages/backend/src/index.ts | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index fa49d5f015..f4cbd9038f 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -21,6 +21,7 @@ import { findPaths } from '@backstage/cli-common'; import { Config, ConfigReader } from '@backstage/config'; import { JsonValue } from '@backstage/types'; import { loadConfig } from '@backstage/config-loader'; +import { setRootLoggerFilteredKeys } from './logging'; export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); @@ -186,6 +187,6 @@ export async function loadBackendConfig(options: { ); config.setConfig(ConfigReader.fromConfigs(configs)); - + setRootLoggerFilteredKeys({ 'secret-1': 'GOATS', 'secret-2': 'SHARKS' }); return config; } diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index d06037863c..ee750da1f6 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -15,11 +15,22 @@ */ import { merge } from 'lodash'; +import { Config } from '@backstage/config'; import * as winston from 'winston'; import { LoggerOptions } from 'winston'; import { coloredFormat } from './formats'; +type FilteredKeys = Record; + let rootLogger: winston.Logger; +let filteredKeys: FilteredKeys; + +/** + { + secret-1: 'integrations.github[0].token', + secrets-2: 'something' + } + */ /** @public */ export function getRootLogger(): winston.Logger { @@ -31,16 +42,31 @@ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; } +export function setRootLoggerFilteredKeys(_filteredKeys: FilteredKeys) { + filteredKeys = _filteredKeys; +} + /** @public */ export function createRootLogger( options: winston.LoggerOptions = {}, env = process.env, ): winston.Logger { + // TODO(Harry/Himanshu): Get the config schema, filter all the configs with @visibility secret, and pass that to winston so that it can mask it in the logs. https://github.com/winstonjs/winston/issues/1079#issuecomment-382861053 + const logger = winston.createLogger( merge( { level: env.LOG_LEVEL || 'info', format: winston.format.combine( + winston.format(info => { + // TODO(Harry/Himanshu): Iterate over all secrets, and substitute info.message string. Or dynamically create regex from all the secrets and do a one time substitution. + // example: info.message = info.message.replace(new RegExp('abc123', 'g'), "**[Redacted: Config integration.github.token]**"); + // Make sure do it in a case-insensitive way + Object.entries(filteredKeys || {}).forEach(([key, value]) => { + info.message = info.message.replace(new RegExp(key, 'g'), value); + }); + return info; + })(), env.NODE_ENV === 'production' ? winston.format.json() : coloredFormat, ), defaultMeta: { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b6c148dfba..b32aa5901e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -88,6 +88,8 @@ async function main() { }); const createEnv = makeCreateEnv(config); + logger.info('hiiiii secret-1'); + const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); const codeCoverageEnv = useHotMemoize(module, () => From ee801b5c2b1e207be84baf5d538c88ca957e2168 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Wed, 29 Sep 2021 11:51:21 +0100 Subject: [PATCH 107/245] feat(config): Added a getMap method to return a flattened map of the loaded config Signed-off-by: Harry Hogg --- packages/config/src/reader.ts | 23 +++++++++++++++++++++++ packages/config/src/types.ts | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index bc8a860c85..0947f8c8ab 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -17,6 +17,7 @@ import { JsonValue, JsonObject } from '@backstage/types'; import { AppConfig, Config } from './types'; import cloneDeep from 'lodash/cloneDeep'; +import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith'; // Update the same pattern in config-loader package if this is changed @@ -118,6 +119,28 @@ export class ConfigReader implements Config { return value as T; } + getMap() { + const map: Record = {}; + + const flatten = (data: JsonValue, path = '') => { + if (isObject(data)) { + Object.entries(data).forEach(([key, value]: [string, any]) => + flatten(value, `${path}${path ? '.' : ''}${key}`), + ); + } else if (Array.isArray(data)) { + data.forEach((value, key) => { + flatten(value, `${path}[${key}]`); + }); + } else { + map[path] = data; + } + }; + + flatten(merge({}, this.fallback?.data, this.data)); + + return map; + } + getOptional(key?: string): T | undefined { const value = this.readValue(key); const fallbackValue = this.fallback?.getOptional(key); diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index a543233277..628178379e 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -65,6 +65,12 @@ export type Config = { */ keys(): string[]; + /** + * Returns a flattened map of the config with the full path to the keys and + * the config value as the value. + */ + getMap(): Record; + /** * Same as `getOptional`, but will throw an error if there's no value for the given key. */ From a2b66f2f4d12fde3e8d2d89fcb1d1f4ead1b6c65 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Wed, 29 Sep 2021 11:52:34 +0100 Subject: [PATCH 108/245] feat(logger): Added redaction filter to the rootLogger Signed-off-by: Harry Hogg --- .../backend-common/src/logging/rootLogger.ts | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index ee750da1f6..edeb235d4d 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -13,24 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { merge } from 'lodash'; -import { Config } from '@backstage/config'; import * as winston from 'winston'; import { LoggerOptions } from 'winston'; import { coloredFormat } from './formats'; -type FilteredKeys = Record; +type RedactionMap = Record; let rootLogger: winston.Logger; -let filteredKeys: FilteredKeys; - -/** - { - secret-1: 'integrations.github[0].token', - secrets-2: 'something' - } - */ +let redactionMap: RedactionMap; /** @public */ export function getRootLogger(): winston.Logger { @@ -42,8 +33,26 @@ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; } -export function setRootLoggerFilteredKeys(_filteredKeys: FilteredKeys) { - filteredKeys = _filteredKeys; +/** @public */ +export function setRedactionMap(newRedactionMap: RedactionMap) { + redactionMap = newRedactionMap; +} + +/** + * A winston formatting function that finds occurrences of filteredKeys + * and replaces them with the corresponding identifier. + */ +export function redactLogLine(info: winston.Logform.TransformableInfo) { + // TODO(hhogg): The logger is created before the config is loaded, + // because the logger is needed in the config loader. There is a risk of + // a secret being logged out during the config loading stage 🤷‍♂️ + if (redactionMap) { + Object.entries(redactionMap || {}).forEach(([key, value]) => { + info.message = info.message.replace(new RegExp(key, 'g'), `{{${value}}}`); + }); + } + + return info; } /** @public */ @@ -51,22 +60,12 @@ export function createRootLogger( options: winston.LoggerOptions = {}, env = process.env, ): winston.Logger { - // TODO(Harry/Himanshu): Get the config schema, filter all the configs with @visibility secret, and pass that to winston so that it can mask it in the logs. https://github.com/winstonjs/winston/issues/1079#issuecomment-382861053 - const logger = winston.createLogger( merge( { level: env.LOG_LEVEL || 'info', format: winston.format.combine( - winston.format(info => { - // TODO(Harry/Himanshu): Iterate over all secrets, and substitute info.message string. Or dynamically create regex from all the secrets and do a one time substitution. - // example: info.message = info.message.replace(new RegExp('abc123', 'g'), "**[Redacted: Config integration.github.token]**"); - // Make sure do it in a case-insensitive way - Object.entries(filteredKeys || {}).forEach(([key, value]) => { - info.message = info.message.replace(new RegExp(key, 'g'), value); - }); - return info; - })(), + winston.format(redactLogLine)(), env.NODE_ENV === 'production' ? winston.format.json() : coloredFormat, ), defaultMeta: { From 94929707e93f5a7be1f05b1138704deda96be2cf Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Wed, 29 Sep 2021 12:00:25 +0100 Subject: [PATCH 109/245] feat(backend-common): Pass the redaction map from the backend config-loader to the logger Signed-off-by: Harry Hogg --- packages/backend-common/package.json | 1 + packages/backend-common/src/config.ts | 47 +++++++++++++++++++++++---- packages/backend/src/index.ts | 3 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 60824f0c40..c0705ae32d 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -36,6 +36,7 @@ "@backstage/integration": "^0.6.7", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", + "@lerna/project": "^4.0.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index f4cbd9038f..ca41d8c1bf 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,10 +18,40 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader } from '@backstage/config'; -import { JsonValue } from '@backstage/types'; -import { loadConfig } from '@backstage/config-loader'; -import { setRootLoggerFilteredKeys } from './logging'; +import { loadConfigSchema, loadConfig } from '@backstage/config-loader'; +import { AppConfig, Config, ConfigReader, JsonValue } from '@backstage/config'; + +import { setRedactionMap } from './logging'; + +// Fetch the schema and get all the secrets to pass to the rootLogger for redaction +const updateRedactionMap = async (configs: AppConfig[], logger: Logger) => { + // Consider all packages in the monorepo when loading in config + const { Project } = require('@lerna/project'); + const project = new Project(); + const packages = await project.getPackages(); + const localPackageNames = packages.map((p: any) => p.name); + + const schema = await loadConfigSchema({ dependencies: localPackageNames }); + const secretAppConfigs = schema.process(configs, { visibility: ['secret'] }); + const secretConfig = ConfigReader.fromConfigs(secretAppConfigs); + const configMap = secretConfig.getMap(); + + logger.info( + `${ + Object.keys(configMap).length + } secrets found in the config which will be redacted`, + ); + + setRedactionMap( + Object.entries(configMap).reduce>( + (map, [key, value]) => { + map[value] = key; + return map; + }, + {}, + ), + ); +}; export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); @@ -90,6 +120,9 @@ export class ObservableConfigProxy implements Config { get(key?: string): T { return this.select(true).get(key); } + getMap() { + return this.config.getMap(); + } getOptional(key?: string): T | undefined { return this.select(false)?.getOptional(key); } @@ -161,12 +194,13 @@ export async function loadBackendConfig(options: { configRoot: paths.targetRoot, configPaths: configPaths.map(opt => resolvePath(opt)), watch: { - onChange(newConfigs) { + async onChange(newConfigs) { options.logger.info( `Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`, ); config.setConfig(ConfigReader.fromConfigs(newConfigs)); + await updateRedactionMap(configs, options.logger); }, stopSignal: new Promise(resolve => { if (currentCancelFunc) { @@ -187,6 +221,7 @@ export async function loadBackendConfig(options: { ); config.setConfig(ConfigReader.fromConfigs(configs)); - setRootLoggerFilteredKeys({ 'secret-1': 'GOATS', 'secret-2': 'SHARKS' }); + await updateRedactionMap(configs, options.logger); + return config; } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b32aa5901e..cda618f4a8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -86,9 +86,8 @@ async function main() { argv: process.argv, logger, }); - const createEnv = makeCreateEnv(config); - logger.info('hiiiii secret-1'); + const createEnv = makeCreateEnv(config); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); From 1be8d2abdbfb0c12ef1dcb199cc2e410b8d9ac5a Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Wed, 29 Sep 2021 12:04:09 +0100 Subject: [PATCH 110/245] docs(changeset): Addded changeset for the sercret redaction Signed-off-by: Harry Hogg --- .changeset/slimy-frogs-allow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slimy-frogs-allow.md diff --git a/.changeset/slimy-frogs-allow.md b/.changeset/slimy-frogs-allow.md new file mode 100644 index 0000000000..41cc0119ef --- /dev/null +++ b/.changeset/slimy-frogs-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Any set configurations which have been tagged with a visibility 'secret', are now redacted from log lines. From 54decb6266b644b932fe6e91bca7ef6f7b703d6c Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Wed, 29 Sep 2021 13:47:13 +0100 Subject: [PATCH 111/245] docs(api-report): Updated API documentation Signed-off-by: Harry Hogg --- packages/backend-common/api-report.md | 6 ++++++ packages/backend-common/src/logging/rootLogger.ts | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2336a57ec3..d38d9a72be 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -472,6 +472,9 @@ export type ReadUrlResponse = { etag?: string; }; +// @public (undocumented) +export type RedactionMap = Record; + // @public export function requestLoggingHandler(logger?: Logger_2): RequestHandler; @@ -539,6 +542,9 @@ export type ServiceBuilder = { start(): Promise; }; +// @public (undocumented) +export function setRedactionMap(newRedactionMap: RedactionMap): void; + // @public (undocumented) export function setRootLogger(newLogger: winston.Logger): void; diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index edeb235d4d..aaf91ff232 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -18,7 +18,8 @@ import * as winston from 'winston'; import { LoggerOptions } from 'winston'; import { coloredFormat } from './formats'; -type RedactionMap = Record; +/** @public */ +export type RedactionMap = Record; let rootLogger: winston.Logger; let redactionMap: RedactionMap; @@ -42,7 +43,7 @@ export function setRedactionMap(newRedactionMap: RedactionMap) { * A winston formatting function that finds occurrences of filteredKeys * and replaces them with the corresponding identifier. */ -export function redactLogLine(info: winston.Logform.TransformableInfo) { +function redactLogLine(info: winston.Logform.TransformableInfo) { // TODO(hhogg): The logger is created before the config is loaded, // because the logger is needed in the config loader. There is a risk of // a secret being logged out during the config loading stage 🤷‍♂️ From 12428bf338deeaa1e32e9bb68092e2f8cf762645 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 30 Sep 2021 13:31:07 +0100 Subject: [PATCH 112/245] fix(config): Use config get() and find secrets using one big RegExp instead Signed-off-by: Harry Hogg --- packages/backend-common/api-report.md | 5 +- packages/backend-common/src/config.ts | 49 +++++++++---------- .../backend-common/src/logging/rootLogger.ts | 16 +++--- packages/config/src/reader.ts | 23 --------- packages/config/src/types.ts | 6 --- 5 files changed, 30 insertions(+), 69 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index d38d9a72be..92271f8521 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -472,9 +472,6 @@ export type ReadUrlResponse = { etag?: string; }; -// @public (undocumented) -export type RedactionMap = Record; - // @public export function requestLoggingHandler(logger?: Logger_2): RequestHandler; @@ -543,7 +540,7 @@ export type ServiceBuilder = { }; // @public (undocumented) -export function setRedactionMap(newRedactionMap: RedactionMap): void; +export function setRedactionList(redactionList: string[]): void; // @public (undocumented) export function setRootLogger(newLogger: winston.Logger): void; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index ca41d8c1bf..39d0ef1096 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,39 +18,36 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { loadConfigSchema, loadConfig } from '@backstage/config-loader'; +import { + loadConfigSchema, + loadConfig, + ConfigSchema, +} from '@backstage/config-loader'; import { AppConfig, Config, ConfigReader, JsonValue } from '@backstage/config'; -import { setRedactionMap } from './logging'; +import { setRedactionList } from './logging'; // Fetch the schema and get all the secrets to pass to the rootLogger for redaction -const updateRedactionMap = async (configs: AppConfig[], logger: Logger) => { - // Consider all packages in the monorepo when loading in config - const { Project } = require('@lerna/project'); - const project = new Project(); - const packages = await project.getPackages(); - const localPackageNames = packages.map((p: any) => p.name); - - const schema = await loadConfigSchema({ dependencies: localPackageNames }); +const updateRedactionMap = ( + schema: ConfigSchema, + configs: AppConfig[], + logger: Logger, +) => { const secretAppConfigs = schema.process(configs, { visibility: ['secret'] }); const secretConfig = ConfigReader.fromConfigs(secretAppConfigs); - const configMap = secretConfig.getMap(); + const values = new Set(); + const data = secretConfig.get(); + + JSON.parse( + JSON.stringify(data), + (_, v) => typeof v === 'string' && values.add(v), + ); logger.info( - `${ - Object.keys(configMap).length - } secrets found in the config which will be redacted`, + `${values.size} secrets found in the config which will be redacted`, ); - setRedactionMap( - Object.entries(configMap).reduce>( - (map, [key, value]) => { - map[value] = key; - return map; - }, - {}, - ), - ); + setRootLoggerRedactionList(Array.from(values)); }; export class ObservableConfigProxy implements Config { @@ -120,9 +117,6 @@ export class ObservableConfigProxy implements Config { get(key?: string): T { return this.select(true).get(key); } - getMap() { - return this.config.getMap(); - } getOptional(key?: string): T | undefined { return this.select(false)?.getOptional(key); } @@ -185,6 +179,9 @@ export async function loadBackendConfig(options: { const args = parseArgs(options.argv); const configPaths: string[] = [args.config ?? []].flat(); + const schema = await loadConfigSchema({ + dependencies: ['@backstage/backend-common'], + }); const config = new ObservableConfigProxy(options.logger); /* eslint-disable-next-line no-restricted-syntax */ diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index aaf91ff232..3691f5cd91 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -19,10 +19,8 @@ import { LoggerOptions } from 'winston'; import { coloredFormat } from './formats'; /** @public */ -export type RedactionMap = Record; - let rootLogger: winston.Logger; -let redactionMap: RedactionMap; +let redactionRegExp: RegExp; /** @public */ export function getRootLogger(): winston.Logger { @@ -35,8 +33,8 @@ export function setRootLogger(newLogger: winston.Logger) { } /** @public */ -export function setRedactionMap(newRedactionMap: RedactionMap) { - redactionMap = newRedactionMap; +export function setRedactionList(redactionList: string[]) { + redactionRegExp = new RegExp(`(${redactionList.join('|')})`, 'g'); } /** @@ -46,11 +44,9 @@ export function setRedactionMap(newRedactionMap: RedactionMap) { function redactLogLine(info: winston.Logform.TransformableInfo) { // TODO(hhogg): The logger is created before the config is loaded, // because the logger is needed in the config loader. There is a risk of - // a secret being logged out during the config loading stage 🤷‍♂️ - if (redactionMap) { - Object.entries(redactionMap || {}).forEach(([key, value]) => { - info.message = info.message.replace(new RegExp(key, 'g'), `{{${value}}}`); - }); + // a secret being logged out during the config loading stage. + if (redactionRegExp) { + info.message = info.message.replace(redactionRegExp, '[REDACTED]'); } return info; diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 0947f8c8ab..bc8a860c85 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -17,7 +17,6 @@ import { JsonValue, JsonObject } from '@backstage/types'; import { AppConfig, Config } from './types'; import cloneDeep from 'lodash/cloneDeep'; -import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith'; // Update the same pattern in config-loader package if this is changed @@ -119,28 +118,6 @@ export class ConfigReader implements Config { return value as T; } - getMap() { - const map: Record = {}; - - const flatten = (data: JsonValue, path = '') => { - if (isObject(data)) { - Object.entries(data).forEach(([key, value]: [string, any]) => - flatten(value, `${path}${path ? '.' : ''}${key}`), - ); - } else if (Array.isArray(data)) { - data.forEach((value, key) => { - flatten(value, `${path}[${key}]`); - }); - } else { - map[path] = data; - } - }; - - flatten(merge({}, this.fallback?.data, this.data)); - - return map; - } - getOptional(key?: string): T | undefined { const value = this.readValue(key); const fallbackValue = this.fallback?.getOptional(key); diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 628178379e..a543233277 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -65,12 +65,6 @@ export type Config = { */ keys(): string[]; - /** - * Returns a flattened map of the config with the full path to the keys and - * the config value as the value. - */ - getMap(): Record; - /** * Same as `getOptional`, but will throw an error if there's no value for the given key. */ From 18d14d655eba82c981698d0f4f94b96afd739312 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 30 Sep 2021 13:32:03 +0100 Subject: [PATCH 113/245] fix(config): Subscribe to config changes instead of piggy backing off the loader watch. Signed-off-by: Harry Hogg --- packages/backend-common/src/config.ts | 8 +++++--- packages/backend-common/src/logging/rootLogger.ts | 1 + packages/backend/src/index.ts | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 39d0ef1096..0e6f993d8f 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -191,13 +191,12 @@ export async function loadBackendConfig(options: { configRoot: paths.targetRoot, configPaths: configPaths.map(opt => resolvePath(opt)), watch: { - async onChange(newConfigs) { + onChange(newConfigs) { options.logger.info( `Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`, ); config.setConfig(ConfigReader.fromConfigs(newConfigs)); - await updateRedactionMap(configs, options.logger); }, stopSignal: new Promise(resolve => { if (currentCancelFunc) { @@ -218,7 +217,10 @@ export async function loadBackendConfig(options: { ); config.setConfig(ConfigReader.fromConfigs(configs)); - await updateRedactionMap(configs, options.logger); + + // Subscribe to config changes and update the redaction list for logging + updateRedactionMap(schema, configs, options.logger); + config.subscribe(() => updateRedactionMap(schema, configs, options.logger)); return config; } diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index 3691f5cd91..114f4b4f48 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { merge } from 'lodash'; import * as winston from 'winston'; import { LoggerOptions } from 'winston'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index cda618f4a8..b6c148dfba 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -86,7 +86,6 @@ async function main() { argv: process.argv, logger, }); - const createEnv = makeCreateEnv(config); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); From f8a7f5723a849195ad5946dfabe18dac7fcc33d0 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Tue, 5 Oct 2021 12:34:27 +0100 Subject: [PATCH 114/245] Removed added dependency Signed-off-by: Harry Hogg --- packages/backend-common/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c0705ae32d..60824f0c40 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -36,7 +36,6 @@ "@backstage/integration": "^0.6.7", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", - "@lerna/project": "^4.0.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", From 3c10980ec5a4052ad15b771395fafb4ef1e7253d Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Tue, 5 Oct 2021 15:17:32 +0100 Subject: [PATCH 115/245] test(rootLogger): Redaction formatting Signed-off-by: Harry Hogg --- .../src/logging/rootLogger.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/logging/rootLogger.test.ts b/packages/backend-common/src/logging/rootLogger.test.ts index 50cf867c85..f24a757155 100644 --- a/packages/backend-common/src/logging/rootLogger.test.ts +++ b/packages/backend-common/src/logging/rootLogger.test.ts @@ -15,7 +15,12 @@ */ import * as winston from 'winston'; -import { createRootLogger, getRootLogger, setRootLogger } from './rootLogger'; +import { + createRootLogger, + getRootLogger, + setRootLogger, + setRedactionList, +} from './rootLogger'; describe('rootLogger', () => { it('can replace the default logger', () => { @@ -30,6 +35,19 @@ describe('rootLogger', () => { ); }); + it('redacts given secrets', () => { + const logger = createRootLogger(); + jest.spyOn(logger, 'write'); + setRedactionList(['SECRET_1', 'SECRET_2']); + logger.info('Logging SECRET_1 and SECRET_2 but not SECRET_3'); + + expect(logger.write).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Logging [REDACTED] and [REDACTED] but not SECRET_3', + }), + ); + }); + describe('createRootLoger', () => { it('creates a new logger', () => { const oldLogger = getRootLogger(); From a9025f70ba765fc85a4af0c4d50b925d594ef898 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 14 Oct 2021 13:30:43 +0100 Subject: [PATCH 116/245] fix(rootLogger): Added util for escaping a regular expression Signed-off-by: Harry Hogg --- .../src/util/escapeRegExp.test.ts | 80 +++++++++++++++++++ .../backend-common/src/util/escapeRegExp.ts | 24 ++++++ 2 files changed, 104 insertions(+) create mode 100644 packages/backend-common/src/util/escapeRegExp.test.ts create mode 100644 packages/backend-common/src/util/escapeRegExp.ts diff --git a/packages/backend-common/src/util/escapeRegExp.test.ts b/packages/backend-common/src/util/escapeRegExp.test.ts new file mode 100644 index 0000000000..60b12739b3 --- /dev/null +++ b/packages/backend-common/src/util/escapeRegExp.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { escapeRegExp } from './escapeRegExp'; + +describe('escapeRegExp', () => { + test('all the characters', () => { + expect(escapeRegExp('^$\\.*+?()[]{}|')).toBe( + '\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|', + ); + }); + + test('character: ^', () => { + expect(escapeRegExp('^')).toBe('\\^'); + }); + + test('character: $', () => { + expect(escapeRegExp('$')).toBe('\\$'); + }); + + test('character: \\', () => { + expect(escapeRegExp('\\')).toBe('\\\\'); + }); + + test('character: .', () => { + expect(escapeRegExp('.')).toBe('\\.'); + }); + + test('character: *', () => { + expect(escapeRegExp('*')).toBe('\\*'); + }); + + test('character: +', () => { + expect(escapeRegExp('+')).toBe('\\+'); + }); + + test('character: ?', () => { + expect(escapeRegExp('?')).toBe('\\?'); + }); + + test('character: (', () => { + expect(escapeRegExp('(')).toBe('\\('); + }); + + test('character: )', () => { + expect(escapeRegExp(')')).toBe('\\)'); + }); + + test('character: [', () => { + expect(escapeRegExp('[')).toBe('\\['); + }); + + test('character: ]', () => { + expect(escapeRegExp(']')).toBe('\\]'); + }); + + test('character: {', () => { + expect(escapeRegExp('{')).toBe('\\{'); + }); + + test('character: }', () => { + expect(escapeRegExp('}')).toBe('\\}'); + }); + + test('character: |', () => { + expect(escapeRegExp('|')).toBe('\\|'); + }); +}); diff --git a/packages/backend-common/src/util/escapeRegExp.ts b/packages/backend-common/src/util/escapeRegExp.ts new file mode 100644 index 0000000000..58a8d2da15 --- /dev/null +++ b/packages/backend-common/src/util/escapeRegExp.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Escapes a given string to be used inside a RegExp. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ +export const escapeRegExp = (text: string) => { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +}; From 488e6145cbe04ac0a7ec8708b26615a0fe9a6ff0 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Thu, 14 Oct 2021 13:33:46 +0100 Subject: [PATCH 117/245] fix(rootLogger): Renamed to setRootLoggerRedactionList and do not expose publically Signed-off-by: Harry Hogg --- packages/backend-common/api-report.md | 3 --- packages/backend-common/src/config.ts | 11 ++++++----- packages/backend-common/src/logging/index.ts | 2 +- .../backend-common/src/logging/rootLogger.test.ts | 10 +++++----- packages/backend-common/src/logging/rootLogger.ts | 10 ++++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 92271f8521..2336a57ec3 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -539,9 +539,6 @@ export type ServiceBuilder = { start(): Promise; }; -// @public (undocumented) -export function setRedactionList(redactionList: string[]): void; - // @public (undocumented) export function setRootLogger(newLogger: winston.Logger): void; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 0e6f993d8f..7a79d1df38 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -23,12 +23,13 @@ import { loadConfig, ConfigSchema, } from '@backstage/config-loader'; -import { AppConfig, Config, ConfigReader, JsonValue } from '@backstage/config'; +import { AppConfig, Config, ConfigReader } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; -import { setRedactionList } from './logging'; +import { setRootLoggerRedactionList } from './logging/rootLogger'; // Fetch the schema and get all the secrets to pass to the rootLogger for redaction -const updateRedactionMap = ( +const updateRedactionList = ( schema: ConfigSchema, configs: AppConfig[], logger: Logger, @@ -219,8 +220,8 @@ export async function loadBackendConfig(options: { config.setConfig(ConfigReader.fromConfigs(configs)); // Subscribe to config changes and update the redaction list for logging - updateRedactionMap(schema, configs, options.logger); - config.subscribe(() => updateRedactionMap(schema, configs, options.logger)); + updateRedactionList(schema, configs, options.logger); + config.subscribe(() => updateRedactionList(schema, configs, options.logger)); return config; } diff --git a/packages/backend-common/src/logging/index.ts b/packages/backend-common/src/logging/index.ts index 71e9618f0c..f50114a9ae 100644 --- a/packages/backend-common/src/logging/index.ts +++ b/packages/backend-common/src/logging/index.ts @@ -15,5 +15,5 @@ */ export * from './formats'; -export * from './rootLogger'; +export { createRootLogger, getRootLogger, setRootLogger } from './rootLogger'; export * from './voidLogger'; diff --git a/packages/backend-common/src/logging/rootLogger.test.ts b/packages/backend-common/src/logging/rootLogger.test.ts index f24a757155..192decb653 100644 --- a/packages/backend-common/src/logging/rootLogger.test.ts +++ b/packages/backend-common/src/logging/rootLogger.test.ts @@ -19,7 +19,7 @@ import { createRootLogger, getRootLogger, setRootLogger, - setRedactionList, + setRootLoggerRedactionList, } from './rootLogger'; describe('rootLogger', () => { @@ -38,17 +38,17 @@ describe('rootLogger', () => { it('redacts given secrets', () => { const logger = createRootLogger(); jest.spyOn(logger, 'write'); - setRedactionList(['SECRET_1', 'SECRET_2']); - logger.info('Logging SECRET_1 and SECRET_2 but not SECRET_3'); + setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'SECRET.3']); + logger.info('Logging SECRET-1 and SECRET_2 and SECRET.3'); expect(logger.write).toHaveBeenCalledWith( expect.objectContaining({ - message: 'Logging [REDACTED] and [REDACTED] but not SECRET_3', + message: 'Logging [REDACTED] and [REDACTED] and [REDACTED]', }), ); }); - describe('createRootLoger', () => { + describe('createRootLogger', () => { it('creates a new logger', () => { const oldLogger = getRootLogger(); const newLogger = createRootLogger(); diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index 114f4b4f48..766746fd3c 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -18,8 +18,8 @@ import { merge } from 'lodash'; import * as winston from 'winston'; import { LoggerOptions } from 'winston'; import { coloredFormat } from './formats'; +import { escapeRegExp } from '../util/escapeRegExp'; -/** @public */ let rootLogger: winston.Logger; let redactionRegExp: RegExp; @@ -33,9 +33,11 @@ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; } -/** @public */ -export function setRedactionList(redactionList: string[]) { - redactionRegExp = new RegExp(`(${redactionList.join('|')})`, 'g'); +export function setRootLoggerRedactionList(redactionList: string[]) { + redactionRegExp = new RegExp( + `(${redactionList.map(escapeRegExp).join('|')})`, + 'g', + ); } /** From deaec1ee3fe5c55b8fdc0b4a7a3c248416d700ac Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Mon, 18 Oct 2021 10:59:21 +0100 Subject: [PATCH 118/245] fix(rootLogger): Only set a regex if there are redactions given, otherwise everything gets redacted Signed-off-by: Harry Hogg --- packages/backend-common/src/logging/rootLogger.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index 766746fd3c..b05763a42c 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -34,10 +34,12 @@ export function setRootLogger(newLogger: winston.Logger) { } export function setRootLoggerRedactionList(redactionList: string[]) { - redactionRegExp = new RegExp( - `(${redactionList.map(escapeRegExp).join('|')})`, - 'g', - ); + if (redactionList.length) { + redactionRegExp = new RegExp( + `(${redactionList.map(escapeRegExp).join('|')})`, + 'g', + ); + } } /** From 4b92638259ff7a4a0a39f3e51a81254f151609b1 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Mon, 18 Oct 2021 11:00:01 +0100 Subject: [PATCH 119/245] test(escapeRegExp): Just an extra test to make sure non-regexy strings don't get escaped Signed-off-by: Harry Hogg --- packages/backend-common/src/util/escapeRegExp.test.ts | 4 ++++ packages/backend-common/src/util/escapeRegExp.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/util/escapeRegExp.test.ts b/packages/backend-common/src/util/escapeRegExp.test.ts index 60b12739b3..13c6ae4a5a 100644 --- a/packages/backend-common/src/util/escapeRegExp.test.ts +++ b/packages/backend-common/src/util/escapeRegExp.test.ts @@ -16,6 +16,10 @@ import { escapeRegExp } from './escapeRegExp'; describe('escapeRegExp', () => { + test('does not escape non-regex characters', () => { + expect(escapeRegExp('Backstage Backstage')).toBe('Backstage Backstage'); + }); + test('all the characters', () => { expect(escapeRegExp('^$\\.*+?()[]{}|')).toBe( '\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|', diff --git a/packages/backend-common/src/util/escapeRegExp.ts b/packages/backend-common/src/util/escapeRegExp.ts index 58a8d2da15..bc78967ebe 100644 --- a/packages/backend-common/src/util/escapeRegExp.ts +++ b/packages/backend-common/src/util/escapeRegExp.ts @@ -20,5 +20,5 @@ * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions */ export const escapeRegExp = (text: string) => { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return text.replace(/[.*+?^${}(\)|[\]\\]/g, '\\$&'); }; From f879e8b2b8f5736dc10e42d4aa36bcad95094a7c Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Mon, 18 Oct 2021 11:43:22 +0100 Subject: [PATCH 120/245] fix(backend): Load all schemas from monorepo packages Signed-off-by: Harry Hogg --- packages/backend-common/package.json | 1 + packages/backend-common/src/config.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 60824f0c40..c0705ae32d 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -36,6 +36,7 @@ "@backstage/integration": "^0.6.7", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", + "@lerna/project": "^4.0.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 7a79d1df38..cfb655cbeb 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -180,14 +180,20 @@ export async function loadBackendConfig(options: { const args = parseArgs(options.argv); const configPaths: string[] = [args.config ?? []].flat(); - const schema = await loadConfigSchema({ - dependencies: ['@backstage/backend-common'], - }); - const config = new ObservableConfigProxy(options.logger); - /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); + // TODO(hhogg): This is fetching _all_ of the packages of the monorepo + // in order to find the secrets for redactions, however we only care about + // the backend ones, we need to find a way to exclude the frontend packages. + const { Project } = require('@lerna/project'); + const project = new Project(paths.targetDir); + const packages = await project.getPackages(); + const schema = await loadConfigSchema({ + dependencies: packages.map((p: any) => p.name), + }); + + const config = new ObservableConfigProxy(options.logger); const configs = await loadConfig({ configRoot: paths.targetRoot, configPaths: configPaths.map(opt => resolvePath(opt)), From 59a7e369a9cc50933fde0df29b96c21984ac37ea Mon Sep 17 00:00:00 2001 From: OscarDHdz Date: Mon, 25 Oct 2021 11:31:24 -0500 Subject: [PATCH 121/245] Scaffolder: update template execution error handling Signed-off-by: OscarDHdz --- .../MultistepJsonForm/MultistepJsonForm.tsx | 10 +++++++--- .../src/components/TemplatePage/TemplatePage.tsx | 11 ++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index f292d4e179..b152e6c72c 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -24,6 +24,7 @@ import { Stepper, Typography, } from '@material-ui/core'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { FormProps, IChangeEvent, UiSchema, withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; @@ -44,7 +45,7 @@ type Props = { formData: Record; onChange: (e: IChangeEvent) => void; onReset: () => void; - onFinish: () => Promise; + onFinish: () => Promise; widgets?: FormProps['widgets']; fields?: FormProps['fields']; }; @@ -113,6 +114,7 @@ export const MultistepJsonForm = ({ }: Props) => { const [activeStep, setActiveStep] = useState(0); const [disableButtons, setDisableButtons] = useState(false); + const errorApi = useApi(errorApiRef); const handleReset = () => { setActiveStep(0); @@ -124,9 +126,11 @@ export const MultistepJsonForm = ({ const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0)); const handleCreate = async () => { setDisableButtons(true); - const success = await onFinish(); - if (!success) { + try { + await onFinish(); + } catch (err) { setDisableButtons(false); + errorApi.post(err); } }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 7b6a217543..80aa4158ab 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -128,15 +128,8 @@ export const TemplatePage = ({ ); const handleCreate = async () => { - try { - const id = await scaffolderApi.scaffold(templateName, formState); - - navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id })); - return true; - } catch (e) { - errorApi.post(e); - return false; - } + const id = await scaffolderApi.scaffold(templateName, formState); + navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id })); }; if (error) { From c6c2942daa1eab026362853afa9bf82363a6c536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 25 Oct 2021 19:11:14 +0200 Subject: [PATCH 122/245] address review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/unlucky-hotels-roll.md | 5 +++++ packages/core-components/api-report.md | 3 +++ .../src/components/DismissableBanner/DismissableBanner.tsx | 6 ++++++ .../src/components/DismissableBanner/index.ts | 5 ++++- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/unlucky-hotels-roll.md diff --git a/.changeset/unlucky-hotels-roll.md b/.changeset/unlucky-hotels-roll.md new file mode 100644 index 0000000000..0ba616c2c3 --- /dev/null +++ b/.changeset/unlucky-hotels-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey` diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 62db9d93fd..cc884c9e8f 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -294,6 +294,9 @@ export type DismissableBannerClassKey = | 'info' | 'error'; +// @public @deprecated (undocumented) +export type DismissbleBannerClassKey = DismissableBannerClassKey; + // @public (undocumented) export function DocsIcon(props: IconComponentProps): JSX.Element; diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index f922689ef4..1213111c9d 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -35,6 +35,12 @@ export type DismissableBannerClassKey = | 'info' | 'error'; +/** + * @public + * @deprecated This type contained a typo, please use DismissableBannerClassKey instead + */ +export type DismissbleBannerClassKey = DismissableBannerClassKey; + const useStyles = makeStyles( (theme: BackstageTheme) => ({ root: { diff --git a/packages/core-components/src/components/DismissableBanner/index.ts b/packages/core-components/src/components/DismissableBanner/index.ts index b760c4d8ce..2c1660ec31 100644 --- a/packages/core-components/src/components/DismissableBanner/index.ts +++ b/packages/core-components/src/components/DismissableBanner/index.ts @@ -15,4 +15,7 @@ */ export { DismissableBanner } from './DismissableBanner'; -export type { DismissableBannerClassKey } from './DismissableBanner'; +export type { + DismissableBannerClassKey, + DismissbleBannerClassKey, +} from './DismissableBanner'; From 61fe752dadfb5499e2ca6e225f551450f742eb68 Mon Sep 17 00:00:00 2001 From: rodion Date: Mon, 25 Oct 2021 22:48:14 +0300 Subject: [PATCH 123/245] fix: sentry plugin corrected allowed period values according https://docs.sentry.io/api/events/list-a-projects-issues/ Signed-off-by: rodion --- .changeset/tough-avocados-give.md | 5 +++++ plugins/sentry/api-report.md | 4 ++-- plugins/sentry/src/api/mock/mock-api.ts | 2 +- plugins/sentry/src/api/mock/sentry-issue-mock.json | 2 +- plugins/sentry/src/api/sentry-issue.ts | 2 +- plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx | 4 ++-- .../src/components/SentryIssuesTable/SentryIssuesTable.tsx | 2 +- .../src/components/SentryIssuesWidget/SentryIssuesWidget.tsx | 2 +- 8 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/tough-avocados-give.md diff --git a/.changeset/tough-avocados-give.md b/.changeset/tough-avocados-give.md new file mode 100644 index 0000000000..08b8a7ea72 --- /dev/null +++ b/.changeset/tough-avocados-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': patch +--- + +fix: sentry-plugin correction for allowed period values diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index 078e89d96d..a23fa112c1 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -75,7 +75,7 @@ export type SentryIssue = { userCount: number; stats: { '24h'?: EventPoint[]; - '12h'?: EventPoint[]; + '14d'?: EventPoint[]; }; culprit: string; title: string; @@ -111,7 +111,7 @@ export const SentryIssuesWidget: ({ query, }: { entity: Entity; - statsFor?: '12h' | '24h' | undefined; + statsFor?: '' | '14d' | '24h' | undefined; variant?: InfoCardVariants | undefined; query?: string | undefined; }) => JSX.Element; diff --git a/plugins/sentry/src/api/mock/mock-api.ts b/plugins/sentry/src/api/mock/mock-api.ts index 5f0c68c1d8..af64d8656a 100644 --- a/plugins/sentry/src/api/mock/mock-api.ts +++ b/plugins/sentry/src/api/mock/mock-api.ts @@ -20,7 +20,7 @@ import mockData from './sentry-issue-mock.json'; function getMockIssue(): SentryIssue { const randomizedStats = { - '12h': new Array(12) + '24h': new Array(24) .fill(0) .map(() => [0, Math.floor(Math.random() * 100)]), }; diff --git a/plugins/sentry/src/api/mock/sentry-issue-mock.json b/plugins/sentry/src/api/mock/sentry-issue-mock.json index 115be77b19..34201d56ee 100644 --- a/plugins/sentry/src/api/mock/sentry-issue-mock.json +++ b/plugins/sentry/src/api/mock/sentry-issue-mock.json @@ -4,7 +4,7 @@ "numComments": 0, "userCount": 0, "stats": { - "12h": [ + "24h": [ [1589450400, 7], [1589454000, 2], [1589457600, 6], diff --git a/plugins/sentry/src/api/sentry-issue.ts b/plugins/sentry/src/api/sentry-issue.ts index 4364edb502..ef0e99adb8 100644 --- a/plugins/sentry/src/api/sentry-issue.ts +++ b/plugins/sentry/src/api/sentry-issue.ts @@ -39,7 +39,7 @@ export type SentryIssue = { userCount: number; stats: { '24h'?: EventPoint[]; - '12h'?: EventPoint[]; + '14d'?: EventPoint[]; }; culprit: string; title: string; diff --git a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx index 001b42f9f3..0448b7cfea 100644 --- a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx +++ b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx @@ -20,8 +20,8 @@ import { Sparklines, SparklinesBars } from 'react-sparklines'; export const ErrorGraph = ({ sentryIssue }: { sentryIssue: SentryIssue }) => { const data = - '12h' in sentryIssue.stats - ? sentryIssue.stats['12h'] + '14d' in sentryIssue.stats + ? sentryIssue.stats['14d'] : sentryIssue.stats['24h']; return ( diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index 637f2c4355..b6b7848dfa 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -59,7 +59,7 @@ const columns: TableColumn[] = [ type SentryIssuesTableProps = { sentryIssues: SentryIssue[]; - statsFor?: '24h' | '12h'; + statsFor?: '24h' | '14d' | ''; }; const SentryIssuesTable = ({ diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx index 349ce61128..00f7ad0b0b 100644 --- a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -41,7 +41,7 @@ export const SentryIssuesWidget = ({ query = '', }: { entity: Entity; - statsFor?: '24h' | '12h'; + statsFor?: '24h' | '14d' | ''; variant?: InfoCardVariants; query?: string; }) => { From 45a2a91a3165aca21aed793f7744326c9a55e29b Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 26 Oct 2021 09:48:56 +0100 Subject: [PATCH 124/245] update api reports after merge Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b40270f317..a94fa8a2a0 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -14,11 +14,9 @@ import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-back import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { JsonObject } from '@backstage/config'; -import { JsonObject as JsonObject_2 } from '@backstage/types'; -import { JsonValue } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; -import { JsonValue as JsonValue_2 } from '@backstage/types'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Octokit } from '@octokit/rest'; From f1e96dc5b11d28c93e971ead275f0e0daefb4183 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 26 Oct 2021 10:38:13 +0200 Subject: [PATCH 125/245] chore/cli: Replace msw with setupRequestMockHandlers Signed-off-by: Johan Haals --- .changeset/famous-foxes-invite.md | 5 +++++ packages/backend-common/src/reading/AzureUrlReader.test.ts | 4 ++-- .../backend-common/src/reading/BitbucketUrlReader.test.ts | 4 ++-- packages/backend-common/src/reading/FetchUrlReader.test.ts | 4 ++-- .../backend-common/src/reading/GithubUrlReader.test.ts | 4 ++-- .../backend-common/src/reading/GitlabUrlReader.test.ts | 4 ++-- .../ExampleComponent/ExampleComponent.test.tsx.hbs | 7 +++++-- .../ExampleFetchComponent.test.tsx.hbs | 4 ++-- .../src/lib/AuthConnector/DefaultAuthConnector.test.ts | 4 ++-- packages/integration/src/bitbucket/core.test.ts | 4 ++-- .../AllureReportComponent/AllureReportComponent.test.tsx | 7 +++++-- plugins/auth-backend/src/providers/oidc/provider.test.ts | 4 ++-- plugins/bitrise/src/api/bitriseApi.client.test.ts | 4 ++-- .../BitriseBuildsTableComponent.test.tsx | 7 +++++-- .../src/microsoftGraph/client.test.ts | 4 ++-- .../src/ingestion/processors/UrlReaderProcessor.test.ts | 4 ++-- .../src/ingestion/processors/github/github.test.ts | 4 ++-- plugins/catalog-graphql/src/graphql/module.test.ts | 4 ++-- plugins/catalog-graphql/src/service/client.test.ts | 4 ++-- plugins/catalog-import/src/api/CatalogImportClient.test.ts | 4 ++-- plugins/fossa/src/api/FossaClient.test.ts | 4 ++-- .../src/components/GithubDeploymentsCard.test.tsx | 7 +++++-- .../src/components/AuditList/AuditListTable.test.tsx | 4 ++-- plugins/lighthouse/src/components/AuditList/index.test.tsx | 4 ++-- plugins/lighthouse/src/components/AuditView/index.test.tsx | 4 ++-- .../lighthouse/src/components/CreateAudit/index.test.tsx | 4 ++-- .../scaffolder/actions/builtin/publish/bitbucket.test.ts | 4 ++-- plugins/sonarqube/src/api/SonarQubeClient.test.ts | 4 ++-- .../src/search/DefaultTechDocsCollator.test.ts | 6 +++--- 29 files changed, 74 insertions(+), 57 deletions(-) create mode 100644 .changeset/famous-foxes-invite.md diff --git a/.changeset/famous-foxes-invite.md b/.changeset/famous-foxes-invite.md new file mode 100644 index 0000000000..5cb9f70e75 --- /dev/null +++ b/.changeset/famous-foxes-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Update usage of msw in default plugin template diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 59968a18e9..23de8389ad 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -19,7 +19,7 @@ import { AzureIntegration, readAzureIntegrationConfig, } from '@backstage/integration'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -51,7 +51,7 @@ describe('AzureUrlReader', () => { }); const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 5bbf3725ea..ea8e4626be 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -19,7 +19,7 @@ import { BitbucketIntegration, readBitbucketIntegrationConfig, } from '@backstage/integration'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -72,7 +72,7 @@ describe('BitbucketUrlReader', () => { }); const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('readUrl', () => { worker.use( diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index e8c16c5f0a..124abb799c 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '../logging'; @@ -28,7 +28,7 @@ const fetchUrlReader = new FetchUrlReader(); describe('FetchUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index c76364976d..ff73718d50 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -20,7 +20,7 @@ import { GitHubIntegration, readGitHubIntegrationConfig, } from '@backstage/integration'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -73,7 +73,7 @@ const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('GithubUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { mockFs({ diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 331150bde3..c8a7e17ae3 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -77,7 +77,7 @@ describe('GitlabUrlReader', () => { }); const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 0044b67a37..ccbebcac40 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -4,12 +4,15 @@ import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from "@backstage/test-utils"; describe('ExampleComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); // setup mock response beforeEach(() => { diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index fa1e289d8e..95533c899d 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -3,12 +3,12 @@ import { render } from '@testing-library/react'; import { ExampleFetchComponent } from './ExampleFetchComponent'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; describe('ExampleFetchComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); // setup mock response beforeEach(() => { diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 28125f6fa8..7e448586e3 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -19,7 +19,7 @@ import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; import { UrlPatternDiscovery } from '../../apis'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; @@ -41,7 +41,7 @@ const defaultOptions = { describe('DefaultAuthConnector', () => { const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); afterEach(() => { jest.resetAllMocks(); diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 0de1d5aaab..2b16be82d4 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { BitbucketIntegrationConfig } from './config'; import { getBitbucketDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucket core', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('getBitbucketRequestOptions', () => { it('inserts a token when needed', () => { diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx index 19c7796e55..34d12aa032 100644 --- a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -19,12 +19,15 @@ import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; describe('ExampleComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); // setup mock response beforeEach(() => { diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index c302c44082..ac5035e9a5 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -15,7 +15,7 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import express from 'express'; import { Session } from 'express-session'; import { JWK, JWT } from 'jose'; @@ -52,7 +52,7 @@ const clientMetadata = { describe('OidcAuthProvider', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/bitrise/src/api/bitriseApi.client.test.ts b/plugins/bitrise/src/api/bitriseApi.client.test.ts index dde4e1ef69..a85be2a9cb 100644 --- a/plugins/bitrise/src/api/bitriseApi.client.test.ts +++ b/plugins/bitrise/src/api/bitriseApi.client.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { BitriseClientApi } from './bitriseApi.client'; @@ -24,7 +24,7 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; const server = setupServer(); describe('BitriseClientApi', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx index 43e596e8d7..a206e28de6 100644 --- a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx +++ b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx @@ -18,7 +18,10 @@ import React from 'react'; import { bitriseApiRef } from '../../plugin'; import { BitriseClientApi } from '../../api/bitriseApi.client'; import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; import { useBitriseBuilds } from '../../hooks/useBitriseBuilds'; import { BitriseBuildsTable } from './BitriseBuildsTableComponent'; import { @@ -34,7 +37,7 @@ jest.mock('../../hooks/useBitriseBuilds', () => ({ const server = setupServer(); describe('BitriseBuildsFetchComponent', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); let apis: ApiRegistry; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index cefa4a52ce..6413a97c15 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import * as msal from '@azure/msal-node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; @@ -28,7 +28,7 @@ describe('MicrosoftGraphClient', () => { let client: MicrosoftGraphClient; const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { confidentialClientApplication.acquireTokenByClientCredential.mockResolvedValue( diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index a63a001082..b7dc0084f2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -20,7 +20,7 @@ import { UrlReaders, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -39,7 +39,7 @@ describe('UrlReaderProcessor', () => { set: jest.fn(), }; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts index e481355fb4..b38153a193 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { graphql } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; import { setupServer } from 'msw/node'; @@ -28,7 +28,7 @@ import { describe('github', () => { const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); describe('getOrganizationUsers', () => { it('reads members', async () => { diff --git a/plugins/catalog-graphql/src/graphql/module.test.ts b/plugins/catalog-graphql/src/graphql/module.test.ts index 3d1219df31..3e835b3111 100644 --- a/plugins/catalog-graphql/src/graphql/module.test.ts +++ b/plugins/catalog-graphql/src/graphql/module.test.ts @@ -21,7 +21,7 @@ import { setupServer } from 'msw/node'; import { ConfigReader } from '@backstage/config'; import { ReaderEntity } from '../service/client'; import { createLogger } from 'winston'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { gql } from 'apollo-server'; describe('Catalog Module', () => { @@ -33,7 +33,7 @@ describe('Catalog Module', () => { }, }); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('Default Entity', () => { beforeEach(() => { diff --git a/plugins/catalog-graphql/src/service/client.test.ts b/plugins/catalog-graphql/src/service/client.test.ts index f10912dc24..8d674a6814 100644 --- a/plugins/catalog-graphql/src/service/client.test.ts +++ b/plugins/catalog-graphql/src/service/client.test.ts @@ -16,11 +16,11 @@ import { CatalogClient } from './client'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; describe('Catalog GraphQL Module', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); const baseUrl = 'http://localhost:1234'; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 0ddd92e121..276b6d351c 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -50,7 +50,7 @@ import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { ScmAuthApi } from '@backstage/integration-react'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -58,7 +58,7 @@ import { CatalogImportClient } from './CatalogImportClient'; describe('CatalogImportClient', () => { const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/catalog'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index 7cb359f979..4debb37ad6 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FindingSummary, FossaApi, FossaClient } from './index'; @@ -40,7 +40,7 @@ const identityApi: IdentityApi = { }; describe('FossaClient', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 4c1b7854c6..08eeb4a3ce 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -16,7 +16,10 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; import { GithubDeployment, GithubDeploymentsApiClient, @@ -127,7 +130,7 @@ const assertFetchedData = async () => { describe('github-deployments', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { worker.resetHandlers(); diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index e9a70bd54b..4c92997755 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp, msw } from '@backstage/test-utils'; +import { wrapInTestApp, setupRequestMockHandlers } from '@backstage/test-utils'; import AuditListTable from './AuditListTable'; import { @@ -36,7 +36,7 @@ describe('AuditListTable', () => { let apis: ApiRegistry; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { apis = ApiRegistry.from([ diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 2781bc4c5b..ea3587fea9 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -23,7 +23,7 @@ jest.mock('react-router-dom', () => { }; }); -import { msw, wrapInTestApp } from '@backstage/test-utils'; +import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -44,7 +44,7 @@ describe('AuditList', () => { let apis: ApiRegistry; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { apis = ApiRegistry.from([ diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index 8a8146199d..ed1bbbcaf9 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -26,7 +26,7 @@ jest.mock('react-router-dom', () => { }; }); -import { msw, wrapInTestApp } from '@backstage/test-utils'; +import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -46,7 +46,7 @@ describe('AuditView', () => { let id: string; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { server.use( diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index ca078c7e84..84493e243f 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -23,7 +23,7 @@ jest.mock('react-router-dom', () => { }; }); -import { msw, wrapInTestApp } from '@backstage/test-utils'; +import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -44,7 +44,7 @@ describe('CreateAudit', () => { let apis: ApiRegistry; let errorApi: ErrorApi; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { errorApi = { post: jest.fn(), error$: jest.fn() }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 4eee4b4b96..2678abd9dd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -19,7 +19,7 @@ jest.mock('../helpers'); import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; @@ -60,7 +60,7 @@ describe('publish:bitbucket', () => { createTemporaryDirectory: jest.fn(), }; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index dbc65ee59e..f8fd7c2264 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FindingSummary, SonarQubeClient } from './index'; @@ -54,7 +54,7 @@ const identityApiGuest: IdentityApi = { }; describe('SonarQubeClient', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 006cfdd073..29c74a119c 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -90,7 +90,7 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { let collator: DefaultTechDocsCollator; const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), @@ -148,7 +148,7 @@ describe('DefaultTechDocsCollator', () => { let collator: DefaultTechDocsCollator; const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), From f18755ee497a7489fcb5a3f28425b51580f90bfb Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Tue, 26 Oct 2021 09:57:26 +0100 Subject: [PATCH 126/245] Using req instead of state Signed-off-by: Nicolas Arnold --- .../src/providers/github/provider.test.ts | 6 +++--- .../auth-backend/src/providers/github/provider.ts | 13 ++++++------- plugins/auth-backend/src/providers/types.ts | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 44467ce66c..e418ab22c2 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -25,7 +25,7 @@ import { } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; -import { OAuthState, encodeState } from '../../lib/oauth'; +import { OAuthStartRequest, encodeState } from '../../lib/oauth'; const mockFrameHandler = jest.spyOn( helpers, @@ -57,8 +57,8 @@ describe('GithubAuthProvider', () => { authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), - stateEncoder: async (state: OAuthState) => ({ - encodedState: encodeState(state), + stateEncoder: async (req: OAuthStartRequest) => ({ + encodedState: encodeState(req.state), }), callbackUrl: 'mock', clientId: 'mock', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index bc81e61a10..c7e82cc5ff 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -42,7 +42,6 @@ import { encodeState, OAuthRefreshRequest, OAuthResponse, - OAuthState, } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; @@ -114,7 +113,7 @@ export class GithubAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, - state: (await this.stateEncoder(req.state)).encodedState, + state: (await this.stateEncoder(req)).encodedState, }); } @@ -286,11 +285,11 @@ export const createGithubProvider = ( logger, }); - const stateEncoder: StateEncoder = options?.stateEncoder - ? options.stateEncoder - : async (state: OAuthState): Promise<{ encodedState: string }> => { - return { encodedState: encodeState(state) }; - }; + const stateEncoder: StateEncoder = + options?.stateEncoder ?? + (async (req: OAuthStartRequest): Promise<{ encodedState: string }> => { + return { encodedState: encodeState(req.state) }; + }); const provider = new GithubAuthProvider({ clientId, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 09e1935f3e..089c82b293 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -21,7 +21,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity/types'; -import { OAuthState } from '../lib/oauth/types'; +import { OAuthStartRequest } from '../lib/oauth/types'; import { CatalogIdentityClient } from '../lib/catalog'; export type AuthProviderConfig = { @@ -226,5 +226,5 @@ export type AuthHandler = ( ) => Promise; export type StateEncoder = ( - input: OAuthState, + req: OAuthStartRequest, ) => Promise<{ encodedState: string }>; From fbedcebe7cf3ed8713e4e7e215b29d7402d38643 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 11:59:00 +0200 Subject: [PATCH 127/245] chore: turn on some debug logging Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 89a46704c2..930cd93f55 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -357,6 +357,7 @@ async function testAppServe(pluginName: string, appDir: string) { for (let attempts = 1; ; attempts++) { try { const browser = new Browser(); + browser.debug(); await waitForPageWithText(browser, '/', 'My Company Catalog'); await waitForPageWithText( From 6f923c9e1d5ea2c0ceec745e6c61be5f3e3e51e1 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 12:00:26 +0200 Subject: [PATCH 128/245] chore: increase the attempts Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 930cd93f55..3db860e647 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -370,7 +370,7 @@ async function testAppServe(pluginName: string, appDir: string) { successful = true; break; } catch (error) { - if (attempts >= 5) { + if (attempts >= 20) { throw new Error(`App serve test failed, ${error}`); } console.log(`App serve failed, trying again, ${error}`); From 82a79983753224790fe2d376816123e972ab151d Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 12:15:33 +0200 Subject: [PATCH 129/245] chore: run the e2e-tests when the correct path changes Signed-off-by: blam --- .github/workflows/e2e-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index f39a89e8c6..62428d29e2 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -7,7 +7,7 @@ on: paths: - '.github/workflows/e2e-win.yml' - 'packages/cli/**' - - 'packages/e2e/**' + - 'packages/e2e-test/**' - 'packages/create-app/**' jobs: From b92b436efcc922c474492fd14b7381f072fad830 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 12:33:19 +0200 Subject: [PATCH 130/245] chore: use the debug module Signed-off-by: blam --- .github/workflows/e2e-win.yml | 2 +- packages/e2e-test/src/commands/run.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 62428d29e2..0e004ca1a6 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -47,4 +47,4 @@ jobs: - name: yarn build run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli - name: run E2E test - run: yarn e2e-test run + run: DEBUG=zombie yarn e2e-test run diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 3db860e647..b0db715f25 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -357,7 +357,6 @@ async function testAppServe(pluginName: string, appDir: string) { for (let attempts = 1; ; attempts++) { try { const browser = new Browser(); - browser.debug(); await waitForPageWithText(browser, '/', 'My Company Catalog'); await waitForPageWithText( From d781df18437df3eb15149ce292158b0527f637ac Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 26 Oct 2021 11:34:13 +0100 Subject: [PATCH 131/245] hide workflow runners behind createWorker Also renames legacy and default workflow runners. Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 19 +---- .../sample-templates/all-templates.yaml | 1 - .../bitbucket-demo/template/catalog-info.yaml | 4 +- .../src/scaffolder/index.ts | 3 +- .../tasks/DefaultWorkflowRunner.test.ts | 6 +- ...wRunner.ts => HandlebarsWorkflowRunner.ts} | 2 +- .../tasks/LegacyWorkflowRunner.test.ts | 6 +- ...lowRunner.ts => NunjucksWorkflowRunner.ts} | 2 +- .../src/scaffolder/tasks/TaskWorker.test.ts | 74 +++++++++++++------ .../src/scaffolder/tasks/TaskWorker.ts | 12 +-- .../src/scaffolder/tasks/types.ts | 5 ++ .../scaffolder-backend/src/service/router.ts | 25 ++----- 12 files changed, 82 insertions(+), 77 deletions(-) rename plugins/scaffolder-backend/src/scaffolder/tasks/{LegacyWorkflowRunner.ts => HandlebarsWorkflowRunner.ts} (99%) rename plugins/scaffolder-backend/src/scaffolder/tasks/{DefaultWorkflowRunner.ts => NunjucksWorkflowRunner.ts} (99%) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a94fa8a2a0..d0f4cb36e8 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -412,6 +412,9 @@ export interface TaskBroker { vacuumTasks(timeoutS: { timeoutS: number }): Promise; } +// @public +export type TaskEventType = 'completion' | 'log'; + // @public export type TaskSecrets = { token: string | undefined; @@ -519,7 +522,6 @@ export type TaskStoreListEventsOptions = { // @public export class TaskWorker { - constructor(options: TaskWorkerOptions); // (undocumented) static createWorker(options: CreateWorkerOptions): TaskWorker; // (undocumented) @@ -528,15 +530,6 @@ export class TaskWorker { start(): void; } -// @public -export type TaskWorkerOptions = { - taskBroker: TaskBroker; - runners: { - legacyWorkflowRunner: LegacyWorkflowRunner; - workflowRunner: WorkflowRunner; - }; -}; - // Warning: (ae-missing-release-tag) "TemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -563,10 +556,4 @@ export class TemplateActionRegistry { action: TemplateAction, ): void; } - -// Warnings were encountered during analysis: -// -// src/scaffolder/tasks/TaskWorker.d.ts:14:9 - (ae-forgotten-export) The symbol "LegacyWorkflowRunner" needs to be exported by the entry point index.d.ts -// src/scaffolder/tasks/TaskWorker.d.ts:15:9 - (ae-forgotten-export) The symbol "WorkflowRunner" needs to be exported by the entry point index.d.ts -// src/scaffolder/tasks/types.d.ts:37:5 - (ae-forgotten-export) The symbol "TaskEventType" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/scaffolder-backend/sample-templates/all-templates.yaml b/plugins/scaffolder-backend/sample-templates/all-templates.yaml index 2d5adb8e3b..6637c9479b 100644 --- a/plugins/scaffolder-backend/sample-templates/all-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/all-templates.yaml @@ -6,7 +6,6 @@ metadata: spec: targets: - ./remote-templates.yaml - # For local development of a template, you can reference your local templates here. # Examples: # diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml index 875664d2a8..3b685a57e0 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: {{cookiecutter.name | jsonify}} + name: { { cookiecutter.name | jsonify } } spec: type: website lifecycle: experimental - owner: {{cookiecutter.owner | jsonify}} + owner: { { cookiecutter.owner | jsonify } } diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index b2599f20ed..6b0e033b7d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -15,7 +15,7 @@ */ export * from './actions'; export { DatabaseTaskStore, TaskWorker } from './tasks'; -export type { TaskWorkerOptions, CreateWorkerOptions } from './tasks'; +export type { CreateWorkerOptions } from './tasks'; export type { TaskState } from './tasks'; export type { TaskSecrets, @@ -28,4 +28,5 @@ export type { TaskSpecV1beta2, TaskSpecV1beta3, Status, + TaskEventType, } from './tasks/types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts index d6d1b0cfd2..0a843f4ddf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts @@ -18,7 +18,7 @@ import mockFs from 'mock-fs'; import * as winston from 'winston'; import { getVoidLogger } from '@backstage/backend-common'; -import { DefaultWorkflowRunner } from './DefaultWorkflowRunner'; +import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; @@ -27,7 +27,7 @@ import { Task, TaskSpec } from './types'; describe('DefaultWorkflowRunner', () => { const logger = getVoidLogger(); let actionRegistry = new TemplateActionRegistry(); - let runner: DefaultWorkflowRunner; + let runner: NunjucksWorkflowRunner; let fakeActionHandler: jest.Mock; const integrations = ScmIntegrations.fromConfig( @@ -88,7 +88,7 @@ describe('DefaultWorkflowRunner', () => { }, }); - runner = new DefaultWorkflowRunner({ + runner = new NunjucksWorkflowRunner({ actionRegistry, integrations, workingDirectory: '/tmp', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts similarity index 99% rename from plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 6d43ae978c..831ef98784 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -47,7 +47,7 @@ const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced * with the default workflow runner interface in the future so this entire thing can go bye bye. */ -export class LegacyWorkflowRunner implements WorkflowRunner { +export class HandlebarsWorkflowRunner implements WorkflowRunner { private readonly handlebars: typeof Handlebars; constructor(private readonly options: Options) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts index 2714f1b80c..aa46747c68 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts @@ -20,12 +20,12 @@ import { createTemplateAction, TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; +import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { Task, TaskSpec } from './types'; import { RepoSpec } from '../actions/builtin/publish/util'; describe('LegacyWorkflowRunner', () => { - let runner: LegacyWorkflowRunner; + let runner: HandlebarsWorkflowRunner; const logger = getVoidLogger(); let actionRegistry = new TemplateActionRegistry(); @@ -60,7 +60,7 @@ describe('LegacyWorkflowRunner', () => { }, }); - runner = new LegacyWorkflowRunner({ + runner = new HandlebarsWorkflowRunner({ actionRegistry, integrations, workingDirectory: '/tmp', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts similarity index 99% rename from plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index e56befd939..5cceb16324 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -77,7 +77,7 @@ const createStepLogger = ({ task, step }: { task: Task; step: TaskStep }) => { return { taskLogger, streamLogger }; }; -export class DefaultWorkflowRunner implements WorkflowRunner { +export class NunjucksWorkflowRunner implements WorkflowRunner { private readonly nunjucks: nunjucks.Environment; private readonly nunjucksOptions: nunjucks.ConfigureOptions = { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index f6b332b6d3..3cb73abb85 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -19,8 +19,20 @@ import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; -import { WorkflowRunner } from './types'; -import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; +import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; +import { ScmIntegrations } from '@backstage/integration'; +import { TemplateActionRegistry } from '../actions'; +import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; + +jest.mock('./HandlebarsWorkflowRunner'); +const MockedHandlebarsWorkflowRunner = + HandlebarsWorkflowRunner as jest.Mock; +MockedHandlebarsWorkflowRunner.mockImplementation(); + +jest.mock('./NunjucksWorkflowRunner'); +const MockedNunjucksWorkflowRunner = + NunjucksWorkflowRunner as jest.Mock; +MockedNunjucksWorkflowRunner.mockImplementation(); async function createStore(): Promise { const manager = DatabaseManager.fromConfig( @@ -40,13 +52,19 @@ async function createStore(): Promise { describe('TaskWorker', () => { let storage: DatabaseTaskStore; - const workflowRunner: WorkflowRunner = { - execute: jest.fn(), - } as unknown as WorkflowRunner; - const legacyWorkflowRunner: LegacyWorkflowRunner = { + const integrations: ScmIntegrations = {} as ScmIntegrations; + + const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry; + const workingDirectory = '/tmp/scaffolder'; + + const handlebarsWorkflowRunner: HandlebarsWorkflowRunner = { execute: jest.fn(), - } as unknown as LegacyWorkflowRunner; + } as unknown as HandlebarsWorkflowRunner; + + const workflowRunner: NunjucksWorkflowRunner = { + execute: jest.fn(), + } as unknown as NunjucksWorkflowRunner; beforeAll(async () => { storage = await createStore(); @@ -54,18 +72,22 @@ describe('TaskWorker', () => { beforeEach(() => { jest.resetAllMocks(); + MockedHandlebarsWorkflowRunner.mockImplementation( + () => handlebarsWorkflowRunner, + ); + MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner); }); const logger = getVoidLogger(); it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = new TaskWorker({ + const taskWorker = TaskWorker.createWorker({ + logger, + workingDirectory, + integrations, taskBroker: broker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, }); await broker.dispatch({ @@ -80,17 +102,23 @@ describe('TaskWorker', () => { const task = await broker.claim(); await taskWorker.runOneTask(task); - expect(legacyWorkflowRunner.execute).toHaveBeenCalled(); + expect(MockedHandlebarsWorkflowRunner).toBeCalledWith({ + actionRegistry, + integrations, + logger, + workingDirectory, + }); + expect(handlebarsWorkflowRunner.execute).toHaveBeenCalled(); }); it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = new TaskWorker({ + const taskWorker = TaskWorker.createWorker({ + logger, + workingDirectory, + integrations, taskBroker: broker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, }); await broker.dispatch({ @@ -114,12 +142,12 @@ describe('TaskWorker', () => { }); const broker = new StorageTaskBroker(storage, logger); - const taskWorker = new TaskWorker({ + const taskWorker = TaskWorker.createWorker({ + logger, + workingDirectory, + integrations, taskBroker: broker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, }); const { taskId } = await broker.dispatch({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 6e70ea06a6..fcc215be99 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -15,8 +15,8 @@ */ import { Task, TaskBroker, WorkflowRunner } from './types'; -import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; -import { DefaultWorkflowRunner } from './DefaultWorkflowRunner'; +import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; +import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { Logger } from 'winston'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; @@ -30,7 +30,7 @@ import { assertError } from '@backstage/errors'; export type TaskWorkerOptions = { taskBroker: TaskBroker; runners: { - legacyWorkflowRunner: LegacyWorkflowRunner; + legacyWorkflowRunner: HandlebarsWorkflowRunner; workflowRunner: WorkflowRunner; }; }; @@ -54,7 +54,7 @@ export type CreateWorkerOptions = { * @public */ export class TaskWorker { - constructor(private readonly options: TaskWorkerOptions) {} + private constructor(private readonly options: TaskWorkerOptions) {} static createWorker(options: CreateWorkerOptions) { const { @@ -65,14 +65,14 @@ export class TaskWorker { workingDirectory, } = options; - const legacyWorkflowRunner = new LegacyWorkflowRunner({ + const legacyWorkflowRunner = new HandlebarsWorkflowRunner({ logger, actionRegistry, integrations, workingDirectory, }); - const workflowRunner = new DefaultWorkflowRunner({ + const workflowRunner = new NunjucksWorkflowRunner({ actionRegistry, integrations, logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index cd4d291719..f2e13923ec 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -49,6 +49,11 @@ export type SerializedTask = { secrets?: TaskSecrets; }; +/** + * TaskEventType + * + * @public + */ export type TaskEventType = 'completion' | 'log'; /** diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 0bc00a2778..fcd5551be2 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -40,8 +40,6 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '../scaffolder/actions'; import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; -import { LegacyWorkflowRunner } from '../scaffolder/tasks/LegacyWorkflowRunner'; -import { DefaultWorkflowRunner } from '../scaffolder/tasks/DefaultWorkflowRunner'; import { TaskBroker, TaskSpec } from '../scaffolder/tasks/types'; /** @@ -98,28 +96,15 @@ export async function createRouter( const taskBroker = options.taskBroker || new StorageTaskBroker(databaseTaskStore, logger); const actionRegistry = new TemplateActionRegistry(); - const legacyWorkflowRunner = new LegacyWorkflowRunner({ - logger, - actionRegistry, - integrations, - workingDirectory, - }); - - const workflowRunner = new DefaultWorkflowRunner({ - actionRegistry, - integrations, - logger, - workingDirectory, - }); const workers = []; for (let i = 0; i < (taskWorkers || 1); i++) { - const worker = new TaskWorker({ + const worker = TaskWorker.createWorker({ taskBroker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, + integrations, + logger, + workingDirectory, }); workers.push(worker); } From 0235260c9f0809111c7d9a282189455bbd4bd955 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 12:56:51 +0200 Subject: [PATCH 132/245] chore: added env Signed-off-by: blam --- .github/workflows/e2e-win.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 0e004ca1a6..73ec0c782d 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -47,4 +47,6 @@ jobs: - name: yarn build run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli - name: run E2E test - run: DEBUG=zombie yarn e2e-test run + run: yarn e2e-test run + env: + DEBUG: zombie From c754c390ee16c4a89a6609fc057772ae9ffd9217 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 26 Oct 2021 12:09:54 +0100 Subject: [PATCH 133/245] fix up exports to follow covention Signed-off-by: Brian Fletcher --- .../scaffolder-backend/src/scaffolder/index.ts | 17 +---------------- .../src/scaffolder/tasks/index.ts | 18 +++++++++++++++--- .../scaffolder-backend/src/service/router.ts | 10 +++++----- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index 6b0e033b7d..e055223d8c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -14,19 +14,4 @@ * limitations under the License. */ export * from './actions'; -export { DatabaseTaskStore, TaskWorker } from './tasks'; -export type { CreateWorkerOptions } from './tasks'; -export type { TaskState } from './tasks'; -export type { - TaskSecrets, - TaskSpec, - CompletedTaskState, - TaskStoreEmitOptions, - TaskStoreListEventsOptions, - SerializedTask, - SerializedTaskEvent, - TaskSpecV1beta2, - TaskSpecV1beta3, - Status, - TaskEventType, -} from './tasks/types'; +export * from './tasks'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 13831ca1bb..d99cbcda6e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -14,8 +14,20 @@ * limitations under the License. */ export { DatabaseTaskStore } from './DatabaseTaskStore'; -export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; -export { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; +export { TaskAgent } from './StorageTaskBroker'; export type { TaskState } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; -export type { TaskWorkerOptions, CreateWorkerOptions } from './TaskWorker'; +export type { CreateWorkerOptions } from './TaskWorker'; +export type { + TaskSecrets, + TaskSpec, + CompletedTaskState, + TaskStoreEmitOptions, + TaskStoreListEventsOptions, + SerializedTask, + SerializedTaskEvent, + TaskSpecV1beta2, + TaskSpecV1beta3, + Status, + TaskEventType, +} from './types'; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index fcd5551be2..700f2c6e6f 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -22,10 +22,12 @@ import { CatalogEntityClient } from '../lib/catalog'; import { validate } from 'jsonschema'; import { DatabaseTaskStore, - StorageTaskBroker, + TemplateActionRegistry, TaskWorker, -} from '../scaffolder/tasks'; -import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry'; + TemplateAction, + createBuiltinActions, +} from '../scaffolder'; +import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; import { ContainerRunner, @@ -38,8 +40,6 @@ import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateAction } from '../scaffolder/actions'; -import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; import { TaskBroker, TaskSpec } from '../scaffolder/tasks/types'; /** From f733a19f8519d962ebecaf8bbb3043d0711abfc2 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 13:38:30 +0200 Subject: [PATCH 134/245] chore: exponential backoff for all errors not just timeouts Signed-off-by: blam --- packages/e2e-test/src/lib/helpers.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index f326311f07..9a9c2010c9 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -131,12 +131,12 @@ export async function waitForPageWithText( browser: any, path: string, text: string, - { intervalMs = 1000, maxLoadAttempts = 50 } = {}, + { intervalMs = 1000, maxFindAttempts = 50 } = {}, ) { - let loadAttempts = 0; + let findAttempts = 0; for (;;) { try { - const waitTimeMs = intervalMs * (Math.log10(loadAttempts + 1) + 1); + const waitTimeMs = intervalMs * (Math.log10(findAttempts + 1) + 1); console.log(`Attempting to load page at ${path}, waiting ${waitTimeMs}`); await new Promise(resolve => setTimeout(resolve, waitTimeMs)); await browser.visit(path); @@ -150,15 +150,12 @@ export async function waitForPageWithText( break; } catch (error) { assertError(error); - if (error.message.match(EXPECTED_LOAD_ERRORS)) { - loadAttempts++; - if (loadAttempts >= maxLoadAttempts) { - throw new Error( - `Failed to load page '${path}', max number of attempts reached`, - ); - } - } else { - throw error; + + findAttempts++; + if (findAttempts >= maxFindAttempts) { + throw new Error( + `Failed to load page '${path}', max number of attempts reached`, + ); } } } From 1af1f76cb8cda4936d341f3c88909d23947e4cad Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 13:40:45 +0200 Subject: [PATCH 135/245] chore: add another exponential delay to wait for the page to load too Signed-off-by: blam --- packages/e2e-test/src/lib/helpers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index 9a9c2010c9..1b42af2110 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -139,8 +139,11 @@ export async function waitForPageWithText( const waitTimeMs = intervalMs * (Math.log10(findAttempts + 1) + 1); console.log(`Attempting to load page at ${path}, waiting ${waitTimeMs}`); await new Promise(resolve => setTimeout(resolve, waitTimeMs)); + await browser.visit(path); + await new Promise(resolve => setTimeout(resolve, waitTimeMs)); + const escapedText = text.replace(/"|\\/g, '\\$&'); browser.assert.evaluate( `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, From 38ad8746c0ac4247c64c3cdbf438ad0853384635 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 13:45:04 +0200 Subject: [PATCH 136/245] chore: fixing typescript EXPECTED_LOAD_ERRORS Signed-off-by: blam --- packages/e2e-test/src/lib/helpers.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index 1b42af2110..d7b3ee4232 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -25,9 +25,6 @@ import { promisify } from 'util'; const execFile = promisify(execFileCb); -const EXPECTED_LOAD_ERRORS = - /ECONNREFUSED|ECONNRESET|did not get to load all resources/; - export function spawnPiped(cmd: string[], options?: SpawnOptions) { function pipeWithPrefix(stream: NodeJS.WriteStream, prefix = '') { return (data: Buffer) => { From 6b615e92c8fbf8e5b1b97a838e95b35a38f9f489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 Oct 2021 12:22:28 +0200 Subject: [PATCH 137/245] clean up exports in core-app-api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/twenty-cups-suffer.md | 5 + packages/core-app-api/api-report.md | 193 +++++------------- .../AlertApi/AlertApiForwarder.ts | 2 + .../AnalyticsApi/NoOpAnalyticsApi.ts | 2 + .../AppThemeApi/AppThemeSelector.ts | 6 + .../DiscoveryApi/UrlPatternDiscovery.ts | 2 + .../implementations/ErrorApi/ErrorAlerter.ts | 2 + .../ErrorApi/ErrorApiForwarder.ts | 2 + .../ErrorApi/UnhandledErrorForwarder.ts | 5 + .../LocalStorageFeatureFlags.tsx | 5 +- .../OAuthRequestApi/OAuthRequestManager.ts | 2 + .../StorageApi/WebStorage.test.ts | 9 +- .../implementations/StorageApi/WebStorage.ts | 15 +- .../auth/atlassian/AtlassianAuth.ts | 9 +- .../implementations/auth/auth0/Auth0Auth.ts | 9 +- .../auth/bitbucket/BitbucketAuth.ts | 9 +- .../implementations/auth/bitbucket/types.ts | 5 + .../implementations/auth/github/GithubAuth.ts | 8 +- .../apis/implementations/auth/github/types.ts | 5 + .../implementations/auth/gitlab/GitlabAuth.ts | 8 +- .../implementations/auth/google/GoogleAuth.ts | 8 +- .../auth/microsoft/MicrosoftAuth.ts | 9 +- .../implementations/auth/oauth2/OAuth2.ts | 19 +- .../apis/implementations/auth/oauth2/types.ts | 5 + .../implementations/auth/okta/OktaAuth.ts | 9 +- .../auth/onelogin/OneLoginAuth.ts | 10 +- .../implementations/auth/saml/SamlAuth.ts | 11 +- .../apis/implementations/auth/saml/types.ts | 5 + .../src/apis/system/ApiFactoryRegistry.ts | 2 + .../src/apis/system/ApiProvider.tsx | 12 +- .../src/apis/system/ApiRegistry.ts | 18 +- .../src/apis/system/ApiResolver.ts | 6 + .../core-app-api/src/apis/system/types.ts | 3 + packages/core-app-api/src/app/createApp.tsx | 4 + packages/core-app-api/src/app/types.ts | 74 ++++++- .../src/routing/FeatureFlagged.tsx | 11 + .../core-app-api/src/routing/FlatRoutes.tsx | 18 +- packages/core-app-api/src/routing/index.ts | 1 + 38 files changed, 325 insertions(+), 203 deletions(-) create mode 100644 .changeset/twenty-cups-suffer.md diff --git a/.changeset/twenty-cups-suffer.md b/.changeset/twenty-cups-suffer.md new file mode 100644 index 0000000000..1dcb494ee8 --- /dev/null +++ b/.changeset/twenty-cups-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Api cleanup, adding `@public` where necessary and tweaking some comments diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 03c29daa01..09d3313c84 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -58,8 +58,6 @@ import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueChange } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; -// Warning: (ae-missing-release-tag) "AlertApiForwarder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class AlertApiForwarder implements AlertApi { // (undocumented) @@ -68,8 +66,6 @@ export class AlertApiForwarder implements AlertApi { post(alert: AlertMessage): void; } -// Warning: (ae-missing-release-tag) "ApiFactoryHolder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ApiFactoryHolder = { get(api: ApiRef): @@ -83,8 +79,6 @@ export type ApiFactoryHolder = { | undefined; }; -// Warning: (ae-missing-release-tag) "ApiFactoryRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class ApiFactoryRegistry implements ApiFactoryHolder { // (undocumented) @@ -109,11 +103,9 @@ export class ApiFactoryRegistry implements ApiFactoryHolder { >(scope: ApiFactoryScope, factory: ApiFactory): boolean; } -// Warning: (ae-missing-release-tag) "ApiProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const ApiProvider: { - ({ apis, children }: PropsWithChildren): JSX.Element; + (props: PropsWithChildren): JSX.Element; propTypes: { apis: PropTypes.Validator< PropTypes.InferProps<{ @@ -124,9 +116,7 @@ export const ApiProvider: { }; }; -// Warning: (ae-missing-release-tag) "ApiRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class ApiRegistry implements ApiHolder { constructor(apis: Map); // Warning: (ae-forgotten-export) The symbol "ApiRegistryBuilder" needs to be exported by the entry point index.d.ts @@ -134,22 +124,14 @@ export class ApiRegistry implements ApiHolder { // (undocumented) static builder(): ApiRegistryBuilder; // Warning: (ae-forgotten-export) The symbol "ApiImpl" needs to be exported by the entry point index.d.ts - // - // (undocumented) static from(apis: ApiImpl[]): ApiRegistry; // (undocumented) get(api: ApiRef): T | undefined; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static with(api: ApiRef, impl: T): ApiRegistry; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen with(api: ApiRef, impl: T): ApiRegistry; } -// Warning: (ae-missing-release-tag) "ApiResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class ApiResolver implements ApiHolder { constructor(factories: ApiFactoryHolder); // (undocumented) @@ -160,9 +142,7 @@ export class ApiResolver implements ApiHolder { ): void; } -// Warning: (ae-missing-release-tag) "AppComponents" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; @@ -173,23 +153,17 @@ export type AppComponents = { SignInPage?: ComponentType; }; -// Warning: (ae-missing-release-tag) "AppConfigLoader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AppConfigLoader = () => Promise; -// Warning: (ae-missing-release-tag) "AppContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AppContext = { getPlugins(): BackstagePlugin[]; getSystemIcon(key: string): IconComponent | undefined; getComponents(): AppComponents; }; -// Warning: (ae-missing-release-tag) "AppOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AppOptions = { apis?: Iterable; icons?: Partial & { @@ -205,9 +179,8 @@ export type AppOptions = { // Warning: (ae-forgotten-export) The symbol "PartialKeys" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "TargetRouteMap" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "KeysWithType" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "AppRouteBinder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export type AppRouteBinder = < ExternalRoutes extends { [name: string]: ExternalRouteRef; @@ -220,9 +193,7 @@ export type AppRouteBinder = < >, ) => void; -// Warning: (ae-missing-release-tag) "AppThemeSelector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class AppThemeSelector implements AppThemeApi { constructor(themes: AppTheme[]); // (undocumented) @@ -237,9 +208,7 @@ export class AppThemeSelector implements AppThemeApi { setActiveThemeId(themeId?: string): void; } -// Warning: (ae-missing-release-tag) "AtlassianAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class AtlassianAuth { // Warning: (ae-forgotten-export) The symbol "OAuthApiCreateOptions" needs to be exported by the entry point index.d.ts // @@ -252,9 +221,7 @@ export class AtlassianAuth { }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "Auth0Auth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class Auth0Auth { // (undocumented) static create({ @@ -266,9 +233,7 @@ export class Auth0Auth { }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; } -// Warning: (ae-missing-release-tag) "BackstageApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BackstageApp = { getPlugins(): BackstagePlugin[]; getSystemIcon(key: string): IconComponent | undefined; @@ -276,19 +241,20 @@ export type BackstageApp = { getRouter(): ComponentType<{}>; }; -// Warning: (ae-missing-release-tag) "BackstagePluginWithAnyOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BackstagePluginWithAnyOutput = Omit< BackstagePlugin, 'output' > & { - output(): (PluginOutput | UnknownPluginOutput)[]; + output(): ( + | PluginOutput + | { + type: string; + } + )[]; }; -// Warning: (ae-missing-release-tag) "BitbucketAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class BitbucketAuth { // (undocumented) static create({ @@ -300,9 +266,7 @@ export class BitbucketAuth { }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "BitbucketSession" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BitbucketSession = { providerInfo: { accessToken: string; @@ -313,9 +277,7 @@ export type BitbucketSession = { backstageIdentity: BackstageIdentity; }; -// Warning: (ae-missing-release-tag) "BootErrorPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BootErrorPageProps = { step: 'load-config' | 'load-chunk'; error: Error; @@ -324,18 +286,13 @@ export type BootErrorPageProps = { export { ConfigReader }; // Warning: (ae-forgotten-export) The symbol "PrivateAppImpl" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function createApp(options?: AppOptions): PrivateAppImpl; -// Warning: (ae-missing-release-tag) "defaultConfigLoader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const defaultConfigLoader: AppConfigLoader; -// Warning: (ae-missing-release-tag) "ErrorAlerter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class ErrorAlerter implements ErrorApi { constructor(alertApi: AlertApi, errorApi: ErrorApi); @@ -352,8 +309,6 @@ export class ErrorAlerter implements ErrorApi { post(error: Error, context?: ErrorContext): void; } -// Warning: (ae-missing-release-tag) "ErrorApiForwarder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class ErrorApiForwarder implements ErrorApi { // (undocumented) @@ -365,23 +320,17 @@ export class ErrorApiForwarder implements ErrorApi { post(error: Error, context?: ErrorContext): void; } -// Warning: (ae-missing-release-tag) "ErrorBoundaryFallbackProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ErrorBoundaryFallbackProps = { plugin?: BackstagePlugin; error: Error; resetError: () => void; }; -// Warning: (ae-missing-release-tag) "FeatureFlagged" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "FeatureFlaggedProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type FeatureFlaggedProps = { children: ReactNode; } & ( @@ -393,15 +342,15 @@ export type FeatureFlaggedProps = { } ); -// Warning: (ae-forgotten-export) The symbol "FlatRoutesProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FlatRoutes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null; -// Warning: (ae-missing-release-tag) "GithubAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type FlatRoutesProps = { + children: ReactNode; +}; + +// @public export class GithubAuth implements OAuthApi, SessionApi { // Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts constructor(sessionManager: SessionManager); @@ -431,9 +380,7 @@ export class GithubAuth implements OAuthApi, SessionApi { signOut(): Promise; } -// Warning: (ae-missing-release-tag) "GithubSession" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type GithubSession = { providerInfo: { accessToken: string; @@ -444,9 +391,7 @@ export type GithubSession = { backstageIdentity: BackstageIdentity; }; -// Warning: (ae-missing-release-tag) "GitlabAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class GitlabAuth { // (undocumented) static create({ @@ -458,9 +403,7 @@ export class GitlabAuth { }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "GoogleAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class GoogleAuth { // (undocumented) static create({ @@ -472,8 +415,6 @@ export class GoogleAuth { }: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "LocalStorageFeatureFlags" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class LocalStorageFeatureFlags implements FeatureFlagsApi { // (undocumented) @@ -486,9 +427,7 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { save(options: FeatureFlagsSaveOptions): void; } -// Warning: (ae-missing-release-tag) "MicrosoftAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class MicrosoftAuth { // (undocumented) static create({ @@ -500,17 +439,13 @@ export class MicrosoftAuth { }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "NoOpAnalyticsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class NoOpAnalyticsApi implements AnalyticsApi { // (undocumented) captureEvent(_event: AnalyticsEvent): void; } -// Warning: (ae-missing-release-tag) "OAuth2" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class OAuth2 implements OAuthApi, @@ -519,8 +454,10 @@ export class OAuth2 BackstageIdentityApi, SessionApi { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options); + constructor(options: { + sessionManager: SessionManager; + scopeTransform: (scopes: string[]) => string[]; + }); // Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -553,9 +490,7 @@ export class OAuth2 signOut(): Promise; } -// Warning: (ae-missing-release-tag) "OAuth2Session" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type OAuth2Session = { providerInfo: { idToken: string; @@ -567,8 +502,6 @@ export type OAuth2Session = { backstageIdentity: BackstageIdentity; }; -// Warning: (ae-missing-release-tag) "OAuthRequestManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class OAuthRequestManager implements OAuthRequestApi { // (undocumented) @@ -577,9 +510,7 @@ export class OAuthRequestManager implements OAuthRequestApi { createAuthRequester(options: AuthRequesterOptions): AuthRequester; } -// Warning: (ae-missing-release-tag) "OktaAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class OktaAuth { // (undocumented) static create({ @@ -591,9 +522,7 @@ export class OktaAuth { }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "OneLoginAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class OneLoginAuth { // Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts // @@ -606,9 +535,7 @@ export class OneLoginAuth { }: CreateOptions_2): typeof oneloginAuthApiRef.T; } -// Warning: (ae-missing-release-tag) "SamlAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { @@ -636,16 +563,12 @@ export class SamlAuth signOut(): Promise; } -// Warning: (ae-missing-release-tag) "SignInPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SignInPageProps = { onResult(result: SignInResult): void; }; -// Warning: (ae-missing-release-tag) "SignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SignInResult = { userId: string; profile: ProfileInfo; @@ -653,15 +576,11 @@ export type SignInResult = { signOut?: () => Promise; }; -// Warning: (ae-missing-release-tag) "UnhandledErrorForwarder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class UnhandledErrorForwarder { static forward(errorApi: ErrorApi, errorContext: ErrorContext): void; } -// Warning: (ae-missing-release-tag) "UrlPatternDiscovery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class UrlPatternDiscovery implements DiscoveryApi { static compile(pattern: string): UrlPatternDiscovery; @@ -669,15 +588,14 @@ export class UrlPatternDiscovery implements DiscoveryApi { getBaseUrl(pluginId: string): Promise; } -// Warning: (ae-missing-release-tag) "WebStorage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class WebStorage implements StorageApi { constructor(namespace: string, errorApi: ErrorApi); - // Warning: (ae-forgotten-export) The symbol "CreateStorageApiOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - static create(options: CreateStorageApiOptions): WebStorage; + static create(options: { + errorApi: ErrorApi; + namespace?: string; + }): WebStorage; // (undocumented) forBucket(name: string): WebStorage; // (undocumented) @@ -692,7 +610,6 @@ export class WebStorage implements StorageApi { // Warnings were encountered during analysis: // -// src/apis/system/ApiProvider.d.ts:9:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts -// src/app/types.d.ts:89:5 - (ae-forgotten-export) The symbol "UnknownPluginOutput" needs to be exported by the entry point index.d.ts -// src/app/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts +// src/apis/system/ApiProvider.d.ts:15:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts +// src/app/types.d.ts:152:5 - (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts index 5e649e2f4c..4e5397f65d 100644 --- a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts @@ -20,6 +20,8 @@ import { PublishSubject } from '../../../lib/subjects'; /** * Base implementation for the AlertApi that simply forwards alerts to consumers. + * + * @public */ export class AlertApiForwarder implements AlertApi { private readonly subject = new PublishSubject(); diff --git a/packages/core-app-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts b/packages/core-app-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts index a3e45006ba..fb324465ca 100644 --- a/packages/core-app-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts +++ b/packages/core-app-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts @@ -17,6 +17,8 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; /** * Base implementation for the AnalyticsApi that does nothing. + * + * @public */ export class NoOpAnalyticsApi implements AnalyticsApi { captureEvent(_event: AnalyticsEvent): void {} diff --git a/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts b/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts index 3dfa43b661..b0303cf29a 100644 --- a/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts +++ b/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts @@ -20,6 +20,12 @@ import { BehaviorSubject } from '../../../lib/subjects'; const STORAGE_KEY = 'theme'; +/** + * Exposes the themes installed in the app, and permits switching the currently + * active theme. + * + * @public + */ export class AppThemeSelector implements AppThemeApi { static createWithStorage(themes: AppTheme[]) { const selector = new AppThemeSelector(themes); diff --git a/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts b/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts index 58e14c9f04..aa921c2595 100644 --- a/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts +++ b/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts @@ -21,6 +21,8 @@ const ERROR_PREFIX = 'Invalid discovery URL pattern,'; /** * UrlPatternDiscovery is a lightweight DiscoveryApi implementation. * It uses a single template string to construct URLs for each plugin. + * + * @public */ export class UrlPatternDiscovery implements DiscoveryApi { /** diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts index 57f1ffab66..2111798d6a 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts @@ -18,6 +18,8 @@ import { ErrorApi, ErrorContext, AlertApi } from '@backstage/core-plugin-api'; /** * Decorates an ErrorApi by also forwarding error messages * to the alertApi with an 'error' severity. + * + * @public */ export class ErrorAlerter implements ErrorApi { constructor( diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts index 0edbd2f353..cd4564a050 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -20,6 +20,8 @@ import { PublishSubject } from '../../../lib/subjects'; /** * Base implementation for the ErrorApi that simply forwards errors to consumers. + * + * @public */ export class ErrorApiForwarder implements ErrorApi { private readonly subject = new PublishSubject<{ diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts index b859148e8e..16e473fb3a 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts @@ -16,6 +16,11 @@ import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; * limitations under the License. */ +/** + * Utility class that helps with error forwarding. + * + * @public + */ export class UnhandledErrorForwarder { /** * Add event listener, such that unhandled errors can be forwarded using an given `ErrorApi` instance diff --git a/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx b/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx index 3ee62b3d6e..2c8470642a 100644 --- a/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx +++ b/packages/core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx @@ -43,7 +43,10 @@ export function validateFlagName(name: string): void { } /** - * Create the FeatureFlags implementation based on the API. + * A feature flags implementation that stores the flags in the browser's local + * storage. + * + * @public */ export class LocalStorageFeatureFlags implements FeatureFlagsApi { private registeredFeatureFlags: FeatureFlag[] = []; diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts index 1cd25c4b56..7e7f3c2507 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts @@ -30,6 +30,8 @@ import { BehaviorSubject } from '../../../lib/subjects'; * The purpose of this class and the API is to read a stream of incoming requests * of OAuth access tokens from different providers with varying scope, and funnel * them all together into a single request for each OAuth provider. + * + * @public */ export class OAuthRequestManager implements OAuthRequestApi { private readonly subject = new BehaviorSubject([]); diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts index 6ae43b6100..f8dd85c05c 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CreateStorageApiOptions, WebStorage } from './WebStorage'; -import { StorageApi } from '@backstage/core-plugin-api'; +import { WebStorage } from './WebStorage'; +import { ErrorApi, StorageApi } from '@backstage/core-plugin-api'; describe('WebStorage Storage API', () => { const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; const createWebStorage = ( - args?: Partial, + args?: Partial<{ + errorApi: ErrorApi; + namespace?: string; + }>, ): StorageApi => { return WebStorage.create({ errorApi: mockErrorApi, diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index 08c4df7e8c..b252e6b726 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -24,18 +24,21 @@ import ObservableImpl from 'zen-observable'; const buckets = new Map(); -export type CreateStorageApiOptions = { - errorApi: ErrorApi; - namespace?: string; -}; - +/** + * An implementation of the storage API, that uses the browser's local storage. + * + * @public + */ export class WebStorage implements StorageApi { constructor( private readonly namespace: string, private readonly errorApi: ErrorApi, ) {} - static create(options: CreateStorageApiOptions): WebStorage { + static create(options: { + errorApi: ErrorApi; + namespace?: string; + }): WebStorage { return new WebStorage(options.namespace ?? '', options.errorApi); } diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts index 423c45bc6c..227d70e494 100644 --- a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts @@ -25,7 +25,12 @@ const DEFAULT_PROVIDER = { icon: AtlassianIcon, }; -class AtlassianAuth { +/** + * Implements the OAuth flow to Atlassian products. + * + * @public + */ +export default class AtlassianAuth { static create({ discoveryApi, environment = 'development', @@ -40,5 +45,3 @@ class AtlassianAuth { }); } } - -export default AtlassianAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index 5f074252bf..69462942e4 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -25,7 +25,12 @@ const DEFAULT_PROVIDER = { icon: Auth0Icon, }; -class Auth0Auth { +/** + * Implements the OAuth flow to Auth0 products. + * + * @public + */ +export default class Auth0Auth { static create({ discoveryApi, environment = 'development', @@ -42,5 +47,3 @@ class Auth0Auth { }); } } - -export default Auth0Auth; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index c88db2a2b0..c9db93906a 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -40,7 +40,12 @@ const DEFAULT_PROVIDER = { icon: BitbucketIcon, }; -class BitbucketAuth { +/** + * Implements the OAuth flow to Bitbucket products. + * + * @public + */ +export default class BitbucketAuth { static create({ discoveryApi, environment = 'development', @@ -57,5 +62,3 @@ class BitbucketAuth { }); } } - -export default BitbucketAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts index 7babfdf686..5309fffcd8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts @@ -16,6 +16,11 @@ import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +/** + * Session information for Bitbucket auth. + * + * @public + */ export type BitbucketSession = { providerInfo: { accessToken: string; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 72b4209a34..01e7e77b8e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -51,7 +51,12 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi, SessionApi { +/** + * Implements the OAuth flow to GitHub products. + * + * @public + */ +export default class GithubAuth implements OAuthApi, SessionApi { static create({ discoveryApi, environment = 'development', @@ -158,4 +163,3 @@ class GithubAuth implements OAuthApi, SessionApi { return new Set(scopeList); } } -export default GithubAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/types.ts b/packages/core-app-api/src/apis/implementations/auth/github/types.ts index f5dae3a064..e1c88b9173 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/types.ts @@ -16,6 +16,11 @@ import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +/** + * Session information for GitHub auth. + * + * @public + */ export type GithubSession = { providerInfo: { accessToken: string; diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index 642600558c..af581d0018 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -25,7 +25,12 @@ const DEFAULT_PROVIDER = { icon: GitlabIcon, }; -class GitlabAuth { +/** + * Implements the OAuth flow to GitLab products. + * + * @public + */ +export default class GitlabAuth { static create({ discoveryApi, environment = 'development', @@ -42,4 +47,3 @@ class GitlabAuth { }); } } -export default GitlabAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts index 39e4a896e4..916c52382e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -27,7 +27,12 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; -class GoogleAuth { +/** + * Implements the OAuth flow to Google products. + * + * @public + */ +export default class GoogleAuth { static create({ discoveryApi, oauthRequestApi, @@ -65,4 +70,3 @@ class GoogleAuth { }); } } -export default GoogleAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index b0f7f42a10..09bd7f67a2 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -25,7 +25,12 @@ const DEFAULT_PROVIDER = { icon: MicrosoftIcon, }; -class MicrosoftAuth { +/** + * Implements the OAuth flow to Microsoft products. + * + * @public + */ +export default class MicrosoftAuth { static create({ environment = 'development', provider = DEFAULT_PROVIDER, @@ -48,5 +53,3 @@ class MicrosoftAuth { }); } } - -export default MicrosoftAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 8f685b9266..6c0759d967 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -33,11 +33,6 @@ import { Observable } from '@backstage/types'; import { OAuth2Session } from './types'; import { OAuthApiCreateOptions } from '../types'; -type Options = { - sessionManager: SessionManager; - scopeTransform: (scopes: string[]) => string[]; -}; - type CreateOptions = OAuthApiCreateOptions & { scopeTransform?: (scopes: string[]) => string[]; }; @@ -59,7 +54,12 @@ const DEFAULT_PROVIDER = { icon: OAuth2Icon, }; -class OAuth2 +/** + * Implements a generic OAuth2 flow for auth. + * + * @public + */ +export default class OAuth2 implements OAuthApi, OpenIdConnectApi, @@ -115,7 +115,10 @@ class OAuth2 private readonly sessionManager: SessionManager; private readonly scopeTransform: (scopes: string[]) => string[]; - constructor(options: Options) { + constructor(options: { + sessionManager: SessionManager; + scopeTransform: (scopes: string[]) => string[]; + }) { this.sessionManager = options.sessionManager; this.scopeTransform = options.scopeTransform; } @@ -176,5 +179,3 @@ class OAuth2 return new Set(scopeTransform(scopeList)); } } - -export default OAuth2; diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts index be0fbf38a2..4ada35846a 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts @@ -16,6 +16,11 @@ import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +/** + * Session information for generic OAuth2 auth. + * + * @public + */ export type OAuth2Session = { providerInfo: { idToken: string; diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts index 5ef9bf04a0..294fb496af 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -37,7 +37,12 @@ const OKTA_OIDC_SCOPES: Set = new Set([ const OKTA_SCOPE_PREFIX: string = 'okta.'; -class OktaAuth { +/** + * Implements the OAuth flow to Okta products. + * + * @public + */ +export default class OktaAuth { static create({ discoveryApi, environment = 'development', @@ -67,5 +72,3 @@ class OktaAuth { }); } } - -export default OktaAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index bb0aebae94..f49ea4cde8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -26,7 +26,6 @@ import { OAuth2 } from '../oauth2'; type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; - environment?: string; provider?: AuthProvider & { id: string }; }; @@ -49,7 +48,12 @@ const OIDC_SCOPES: Set = new Set([ const SCOPE_PREFIX: string = 'onelogin.'; -class OneLoginAuth { +/** + * Implements a OneLogin OAuth flow. + * + * @public + */ +export default class OneLoginAuth { static create({ discoveryApi, environment = 'development', @@ -78,5 +82,3 @@ class OneLoginAuth { }); } } - -export default OneLoginAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index 0a7fb50101..f2668dedd2 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -45,7 +45,14 @@ const DEFAULT_PROVIDER = { icon: SamlIcon, }; -class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { +/** + * Implements a general SAML based auth flow. + * + * @public + */ +export default class SamlAuth + implements ProfileInfoApi, BackstageIdentityApi, SessionApi +{ static create({ discoveryApi, environment = 'development', @@ -94,5 +101,3 @@ class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { return session?.profile; } } - -export default SamlAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index 70cbf41ee4..724f0aa5d5 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -15,6 +15,11 @@ */ import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; +/** + * Session information for SAML auth. + * + * @public + */ export type SamlSession = { userId: string; profile: ProfileInfo; diff --git a/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts b/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts index 28ed9f85a1..880f075930 100644 --- a/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts +++ b/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts @@ -44,6 +44,8 @@ type FactoryTuple = { * * Each scope has an assigned priority, where factories registered with * higher priority scopes override ones with lower priority. + * + * @public */ export class ApiFactoryRegistry implements ApiFactoryHolder { private readonly factories = new Map(); diff --git a/packages/core-app-api/src/apis/system/ApiProvider.tsx b/packages/core-app-api/src/apis/system/ApiProvider.tsx index aa57fa9310..73bbdd6f77 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.tsx @@ -30,10 +30,14 @@ type ApiProviderProps = { const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context'); -export const ApiProvider = ({ - apis, - children, -}: PropsWithChildren) => { +/** + * Provides an {@link @backstage/core-plugin-api#ApiHolder} for consumption in + * the React tree. + * + * @public + */ +export const ApiProvider = (props: PropsWithChildren) => { + const { apis, children } = props; const parentHolder = useContext(ApiContext)?.atVersion(1); const holder = parentHolder ? new ApiAggregator(apis, parentHolder) : apis; diff --git a/packages/core-app-api/src/apis/system/ApiRegistry.ts b/packages/core-app-api/src/apis/system/ApiRegistry.ts index 7669810a1e..29c8b1cfe4 100644 --- a/packages/core-app-api/src/apis/system/ApiRegistry.ts +++ b/packages/core-app-api/src/apis/system/ApiRegistry.ts @@ -32,11 +32,21 @@ class ApiRegistryBuilder { } } +/** + * A registry for utility APIs. + * + * @public + */ export class ApiRegistry implements ApiHolder { static builder() { return new ApiRegistryBuilder(); } + /** + * Creates a new ApiRegistry with a list of API implementations. + * + * @param apis - A list of pairs mapping an ApiRef to its respective implementation + */ static from(apis: ApiImpl[]) { return new ApiRegistry(new Map(apis.map(([api, impl]) => [api.id, impl]))); } @@ -44,8 +54,8 @@ export class ApiRegistry implements ApiHolder { /** * Creates a new ApiRegistry with a single API implementation. * - * @param api ApiRef for the API to add - * @param impl Implementation of the API to add + * @param api - ApiRef for the API to add + * @param impl - Implementation of the API to add */ static with(api: ApiRef, impl: T): ApiRegistry { return new ApiRegistry(new Map([[api.id, impl]])); @@ -56,8 +66,8 @@ export class ApiRegistry implements ApiHolder { /** * Returns a new ApiRegistry with the provided API added to the existing ones. * - * @param api ApiRef for the API to add - * @param impl Implementation of the API to add + * @param api - ApiRef for the API to add + * @param impl - Implementation of the API to add */ with(api: ApiRef, impl: T): ApiRegistry { return new ApiRegistry(new Map([...this.apis, [api.id, impl]])); diff --git a/packages/core-app-api/src/apis/system/ApiResolver.ts b/packages/core-app-api/src/apis/system/ApiResolver.ts index dd29ff5010..0c050ebc30 100644 --- a/packages/core-app-api/src/apis/system/ApiResolver.ts +++ b/packages/core-app-api/src/apis/system/ApiResolver.ts @@ -22,6 +22,12 @@ import { } from '@backstage/core-plugin-api'; import { ApiFactoryHolder } from './types'; +/** + * Handles the actual on-demand instantiation and memoization of APIs out of + * an {@link ApiFactoryHolder}. + * + * @public + */ export class ApiResolver implements ApiHolder { /** * Validate factories by making sure that each of the apis can be created diff --git a/packages/core-app-api/src/apis/system/types.ts b/packages/core-app-api/src/apis/system/types.ts index bb0b9d42ea..de4b7cb699 100644 --- a/packages/core-app-api/src/apis/system/types.ts +++ b/packages/core-app-api/src/apis/system/types.ts @@ -16,6 +16,9 @@ import { ApiFactory, ApiRef } from '@backstage/core-plugin-api'; +/** + * @public + */ export type ApiFactoryHolder = { get( api: ApiRef, diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 901697edba..65aa15b50c 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -48,6 +48,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; * which can be rewritten at runtime to contain an additional JSON config object. * If runtime config is present, it will be placed first in the config array, overriding * other config values. + * + * @public */ export const defaultConfigLoader: AppConfigLoader = async ( // This string may be replaced at runtime to provide additional config. @@ -100,6 +102,8 @@ export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) { /** * Creates a new Backstage App. + * + * @public */ export function createApp(options?: AppOptions) { const DefaultNotFoundPage = () => ( diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 09121f8105..407018111b 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -29,11 +29,21 @@ import { import { AppConfig } from '@backstage/config'; import { AppIcons } from './icons'; +/** + * Props for the `BootErrorPage` component of {@link AppComponents}. + * + * @public + */ export type BootErrorPageProps = { step: 'load-config' | 'load-chunk'; error: Error; }; +/** + * The outcome of signing in on the sign-in page. + * + * @public + */ export type SignInResult = { /** * User ID that will be returned by the IdentityApi @@ -53,6 +63,11 @@ export type SignInResult = { signOut?: () => Promise; }; +/** + * Props for the `SignInPage` component of {@link AppComponents}. + * + * @public + */ export type SignInPageProps = { /** * Set the sign-in result for the app. This should only be called once. @@ -60,12 +75,22 @@ export type SignInPageProps = { onResult(result: SignInResult): void; }; +/** + * Props for the fallback error boundary. + * + * @public + */ export type ErrorBoundaryFallbackProps = { plugin?: BackstagePlugin; error: Error; resetError: () => void; }; +/** + * A set of replaceable core components that are part of every Backstage app. + * + * @public + */ export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; @@ -91,6 +116,8 @@ export type AppComponents = { * * If multiple config objects are returned in the array, values in the earlier configs * will override later ones. + * + * @public */ export type AppConfigLoader = () => Promise; @@ -123,6 +150,12 @@ type TargetRouteMap< : never; }; +/** + * A function that can bind from external routes of a given plugin, to concrete + * routes of other plugins. See {@link createApp}. + * + * @public + */ export type AppRouteBinder = < ExternalRoutes extends { [name: string]: ExternalRouteRef }, >( @@ -133,21 +166,33 @@ export type AppRouteBinder = < >, ) => void; -// Output from newer or older plugin API versions that might not be supported by -// this version of the app API, but we don't want to break at the type checking level. -// We only use this more permissive type for the `createApp` options, as we otherwise -// want to stick to using the type for the outputs that we know about in this version -// of the app api. -type UnknownPluginOutput = { - type: string; -}; +/** + * Internal helper type that represents a plugin with any type of output. + * + * @public + * @remarks + * + * The `type: string` type is there to handle output from newer or older plugin + * API versions that might not be supported by this version of the app API, but + * we don't want to break at the type checking level. We only use this more + * permissive type for the `createApp` options, as we otherwise want to stick + * to using the type for the outputs that we know about in this version of the + * app api. + * + * TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet + */ export type BackstagePluginWithAnyOutput = Omit< BackstagePlugin, 'output' > & { - output(): (PluginOutput | UnknownPluginOutput)[]; + output(): (PluginOutput | { type: string })[]; }; +/** + * The options accepted by {@link createApp}. + * + * @public + */ export type AppOptions = { /** * A collection of ApiFactories to register in the application to either @@ -226,6 +271,11 @@ export type AppOptions = { bindRoutes?(context: { bind: AppRouteBinder }): void; }; +/** + * The public API of the output of {@link createApp}. + * + * @public + */ export type BackstageApp = { /** * Returns all plugins registered for the app. @@ -250,6 +300,12 @@ export type BackstageApp = { getRouter(): ComponentType<{}>; }; +/** + * The central context providing runtime app specific state that plugin views + * want to consume. + * + * @public + */ export type AppContext = { /** * Get a list of all plugins that are installed in the app. diff --git a/packages/core-app-api/src/routing/FeatureFlagged.tsx b/packages/core-app-api/src/routing/FeatureFlagged.tsx index 47561d4135..2b0b5afe34 100644 --- a/packages/core-app-api/src/routing/FeatureFlagged.tsx +++ b/packages/core-app-api/src/routing/FeatureFlagged.tsx @@ -21,11 +21,22 @@ import { } from '@backstage/core-plugin-api'; import React, { ReactNode } from 'react'; +/** + * Props for the {@link FeatureFlagged} component. + * + * @public + */ export type FeatureFlaggedProps = { children: ReactNode } & ( | { with: string } | { without: string } ); +/** + * Enables or disables rendering of its children based on the state of a given + * feature flag. + * + * @public + */ export const FeatureFlagged = (props: FeatureFlaggedProps) => { const { children } = props; const featureFlagApi = useApi(featureFlagsApiRef); diff --git a/packages/core-app-api/src/routing/FlatRoutes.tsx b/packages/core-app-api/src/routing/FlatRoutes.tsx index 315ec91cdb..02dbc301fb 100644 --- a/packages/core-app-api/src/routing/FlatRoutes.tsx +++ b/packages/core-app-api/src/routing/FlatRoutes.tsx @@ -24,10 +24,26 @@ type RouteObject = { children?: RouteObject[]; }; -type FlatRoutesProps = { +/** + * Props for the {@link FlatRoutes} component. + * + * @public + */ +export type FlatRoutesProps = { children: ReactNode; }; +/** + * A wrapper around a set of routes. + * + * @remarks + * + * The root of the routing hierarchy in your app should use this component, + * instead of the one from `react-router-dom`. This ensures that all of the + * plugin route and utility API wiring happens under the hood. + * + * @public + */ export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { const app = useApp(); const { NotFoundErrorPage } = app.getComponents(); diff --git a/packages/core-app-api/src/routing/index.ts b/packages/core-app-api/src/routing/index.ts index 4169fffc88..46b5a6f9b4 100644 --- a/packages/core-app-api/src/routing/index.ts +++ b/packages/core-app-api/src/routing/index.ts @@ -15,5 +15,6 @@ */ export { FlatRoutes } from './FlatRoutes'; +export type { FlatRoutesProps } from './FlatRoutes'; export { FeatureFlagged } from './FeatureFlagged'; export type { FeatureFlaggedProps } from './FeatureFlagged'; From c2d50a0073f8941ccd4ead9711606bfaec36250b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 Oct 2021 14:13:17 +0200 Subject: [PATCH 138/245] Add docs to the last export of cli-common MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/great-tomatoes-cheat.md | 5 +++++ packages/cli-common/api-report.md | 2 +- packages/cli-common/src/paths.ts | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/great-tomatoes-cheat.md diff --git a/.changeset/great-tomatoes-cheat.md b/.changeset/great-tomatoes-cheat.md new file mode 100644 index 0000000000..547e89c6d0 --- /dev/null +++ b/.changeset/great-tomatoes-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-common': patch +--- + +Add docs to the last export of cli-common diff --git a/packages/cli-common/api-report.md b/packages/cli-common/api-report.md index 371671e63a..4b61cfedb8 100644 --- a/packages/cli-common/api-report.md +++ b/packages/cli-common/api-report.md @@ -21,6 +21,6 @@ export type Paths = { resolveTargetRoot: ResolveFunc; }; -// @public (undocumented) +// @public export type ResolveFunc = (...paths: string[]) => string; ``` diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 4aa9d4b76e..c9f486752f 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -17,7 +17,12 @@ import fs from 'fs'; import { dirname, resolve as resolvePath } from 'path'; -/** @public */ +/** + * A function that takes a set of path fragments and resolves them into a + * single complete path, relative to some root. + * + * @public + */ export type ResolveFunc = (...paths: string[]) => string; /** From 65bf24bc1db2873a72bfcfa97b0b33a6a4572958 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 14:27:33 +0200 Subject: [PATCH 139/245] chore: removing the log10 and just do linear backoff Signed-off-by: blam --- packages/e2e-test/src/lib/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index d7b3ee4232..5193db08ec 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -133,7 +133,7 @@ export async function waitForPageWithText( let findAttempts = 0; for (;;) { try { - const waitTimeMs = intervalMs * (Math.log10(findAttempts + 1) + 1); + const waitTimeMs = intervalMs * (findAttempts + 1); console.log(`Attempting to load page at ${path}, waiting ${waitTimeMs}`); await new Promise(resolve => setTimeout(resolve, waitTimeMs)); From 6c1a26a8d2bec9ad293608e0be353cc8bf9cb5f2 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 15:40:47 +0200 Subject: [PATCH 140/245] =?UTF-8?q?chore:=20goodbye=20old=20core=20version?= =?UTF-8?q?s=20=F0=9F=91=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: blam --- package.json | 1 - yarn.lock | 242 ++------------------------------------------------- 2 files changed, 8 insertions(+), 235 deletions(-) diff --git a/package.json b/package.json index 4aa6905085..f5d20fd39d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "resolutions": { "**/@graphql-codegen/cli/**/ws": "^7.4.6", - "**/@roadiehq/**/@backstage/core": "*", "**/@roadiehq/**/@backstage/plugin-catalog": "*", "**/@roadiehq/**/@backstage/catalog-model": "*", "graphql-language-service-interface": "2.8.2", diff --git a/yarn.lock b/yarn.lock index c905fbea6f..205e829555 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2071,7 +2071,7 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.15.4" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== @@ -2175,24 +2175,6 @@ "@backstage/errors" "^0.1.2" cross-fetch "^3.0.6" -"@backstage/core-api@^0.2.23": - version "0.2.23" - resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.23.tgz#c0ec13407ff7c78d376eb18e9d8e1490d54d995b" - integrity sha512-859IGJ5LpcFaqZOenJNM9eFBKd5lrdBjYst8I0srLCaZkBCshTbUT615G3zoDMDiXZNSm+h4V82kMT4eES9wDw== - dependencies: - "@backstage/config" "^0.1.4" - "@backstage/core-plugin-api" "^0.1.3" - "@backstage/theme" "^0.2.8" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - prop-types "^15.7.2" - react "^16.12.0" - react-router-dom "6.0.0-beta.0" - react-use "^17.2.4" - zen-observable "^0.8.15" - "@backstage/core-components@^0.3.0": version "0.3.3" resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.3.3.tgz#ec63eac6d789303f90219857849fa1cefe4e0dde" @@ -2371,52 +2353,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/core@*": - version "0.7.14" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.14.tgz#863844fe40bb6a29bcc2d297e42055633b0e886f" - integrity sha512-W7EMspBXrp1GPALK6+qdJjsvkqcaYGFyoh8/bRAXABIkJpGQGiy4xUZUKDoMhd+OdVHv/mzyyv3fH2yc32J07w== - dependencies: - "@backstage/config" "^0.1.5" - "@backstage/core-api" "^0.2.23" - "@backstage/errors" "^0.1.1" - "@backstage/theme" "^0.2.8" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@testing-library/react-hooks" "^3.4.2" - "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - "@types/react-text-truncate" "^0.14.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - history "^5.0.0" - immer "^9.0.1" - lodash "^4.17.15" - material-table "^1.69.1" - pluralize "^8.0.0" - prop-types "^15.7.2" - qs "^6.9.4" - rc-progress "^3.0.0" - react "^16.12.0" - react-dom "^16.12.0" - react-helmet "6.1.0" - react-hook-form "^6.15.4" - react-markdown "^5.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-sparklines "^1.7.0" - react-syntax-highlighter "^15.4.3" - react-text-truncate "^0.16.0" - react-use "^17.2.4" - remark-gfm "^1.0.0" - zen-observable "^0.8.15" - "@backstage/plugin-catalog-react@^0.4.0": version "0.4.6" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.4.6.tgz#f33f3cd734f110d3c547f7eae4e25d14806ec796" @@ -2719,7 +2655,7 @@ enabled "2.0.x" kuler "^2.0.0" -"@date-io/core@1.x", "@date-io/core@^1.1.0", "@date-io/core@^1.3.13": +"@date-io/core@1.x", "@date-io/core@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== @@ -2734,13 +2670,6 @@ resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.11.tgz#b1a3d57730f3eaaab54d5658be4a71727297e357" integrity sha512-keXQnwH0LM8wyvu+j5Z2KGK56D+eItjy7DnwuWl/oV+DM2UEYl0z5WhdPMpfswSyt/kjuPOzcVF/7u/skMLaoA== -"@date-io/date-fns@1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.1.0.tgz#91cd7703042513a6a0056a344b25cd74a4df643e" - integrity sha512-FMRhYWfoGiIXdN4xWAArpkdEbqsg2Fr+6Yda7Np2eVWCNx6gSMYsHIM51IIcI+3762ajYbhoEYjHYXVFNZIk1g== - dependencies: - "@date-io/core" "^1.1.0" - "@date-io/date-fns@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735" @@ -4504,18 +4433,6 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@material-ui/pickers@3.2.2": - version "3.2.2" - resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.2.tgz#9b7a67f5a8f21f8183853ebc1848e5d8dd680d4c" - integrity sha512-on/J1yyKeJ4CkLnItpf/jPDKMZVWvHDklkh5FS7wkZ0s1OPoqTsPubLWfA7eND6xREnVRyLFzVTlE3VlWYdQWw== - dependencies: - "@babel/runtime" "^7.2.0" - "@types/styled-jsx" "^2.2.8" - clsx "^1.0.2" - react-transition-group "^4.0.0" - rifm "^0.7.0" - tslib "^1.9.3" - "@material-ui/pickers@^3.2.10", "@material-ui/pickers@^3.3.10": version "3.3.10" resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz#f1b0f963348cc191645ef0bdeff7a67c6aa25485" @@ -7506,11 +7423,6 @@ resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== -"@types/raf@^3.4.0": - version "3.4.0" - resolved "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" - integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== - "@types/range-parser@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" @@ -7604,7 +7516,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.9": +"@types/react@*", "@types/react@>=16.9.0": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== @@ -9711,16 +9623,6 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-arraybuffer@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" - integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== - -base64-arraybuffer@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c" - integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA== - base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -10480,20 +10382,6 @@ canvas@^2.6.1: nan "^2.14.0" simple-get "^3.0.3" -canvg@^3.0.6: - version "3.0.9" - resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.9.tgz#9ba095f158b94b97ca2c9c1c40785b11dc08df6d" - integrity sha512-rDXcnRPuz4QHoCilMeoTxql+fvGqNAxp+qV/KHD8rOiJSAfVjFclbdUNHD2Uqfthr+VMg17bD2bVuk6F07oLGw== - dependencies: - "@babel/runtime" "^7.12.5" - "@types/raf" "^3.4.0" - core-js "^3.8.3" - raf "^3.4.1" - regenerator-runtime "^0.13.7" - rgbcolor "^1.0.1" - stackblur-canvas "^2.0.0" - svg-pathdata "^6.0.3" - capital-case@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" @@ -10760,11 +10648,6 @@ classnames@*, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1: resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== -classnames@2.2.6: - version "2.2.6" - resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== - clean-css@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" @@ -11560,11 +11443,6 @@ core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2: resolved "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044" integrity sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw== -core-js@^3.6.0, core-js@^3.8.3: - version "3.18.3" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz#86a0bba2d8ec3df860fefcc07a8d119779f01509" - integrity sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -11816,13 +11694,6 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" -css-line-break@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz#3dc74c2ed5eb64211480281932475790243e7338" - integrity sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w== - dependencies: - base64-arraybuffer "^0.2.0" - css-loader@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" @@ -12354,11 +12225,6 @@ date-and-time@^1.0.0: resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-1.0.0.tgz#0062394bdf6f44e961f0db00511cb19cdf3cc0a5" integrity sha512-477D7ypIiqlXBkxhU7YtG9wWZJEQ+RUpujt2quTfgf4+E8g5fNUkB0QIL0bVyP5/TKBg8y55Hfa1R/c4bt3dEw== -date-fns@2.0.0-alpha.27: - version "2.0.0-alpha.27" - resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.27.tgz#5ecd4204ef0e7064264039570f6e8afbc014481c" - integrity sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg== - date-fns@^1.27.2: version "1.30.1" resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" @@ -12384,7 +12250,7 @@ dayjs@^1.10.4: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== -debounce@1.2.0, debounce@^1.2.0: +debounce@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== @@ -12915,7 +12781,7 @@ domhandler@^4.0.0: dependencies: domelementtype "^2.1.0" -dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.9: +dompurify@^2.1.1, dompurify@^2.2.9: version "2.3.3" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== @@ -14101,11 +13967,6 @@ fast-decode-uri-component@^1.0.0: resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== -fast-deep-equal@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - fast-deep-equal@^3.0.0, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -14308,11 +14169,6 @@ file-uri-to-path@1.0.0: resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -filefy@0.1.10: - version "0.1.10" - resolved "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz#174677c8e2fa5bc39a3af0ed6fb492f16b8fbf42" - integrity sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ== - filelist@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" @@ -15956,14 +15812,6 @@ html-webpack-plugin@^5.3.1: pretty-error "^2.1.1" tapable "^2.0.0" -html2canvas@^1.0.0-rc.5: - version "1.3.2" - resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.3.2.tgz#951cc8388a3ce939fdac02131007ee28124afc27" - integrity sha512-4+zqv87/a1LsaCrINV69wVLGG8GBZcYBboz1JPWEgiXcWoD9kroLzccsBRU/L9UlfV2MAZ+3J92U9IQPVMDeSQ== - dependencies: - css-line-break "2.0.1" - text-segmentation "^1.0.2" - htmlparser2@^3.3.0: version "3.10.1" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" @@ -18220,24 +18068,6 @@ jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" -jspdf-autotable@3.5.9: - version "3.5.9" - resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz#8a625ef2aead44271da95e9f649843c401536925" - integrity sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ== - -jspdf@2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz#2322f8644bc41845b3abe20db4c3ca0adeadb84c" - integrity sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ== - dependencies: - atob "^2.1.2" - btoa "^1.2.1" - optionalDependencies: - canvg "^3.0.6" - core-js "^3.6.0" - dompurify "^2.0.12" - html2canvas "^1.0.0-rc.5" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -19156,7 +18986,7 @@ longest-streak@^3.0.0: resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.0.tgz#f127e2bded83caa6a35ac5f7a2f2b2f94b36f3dc" integrity sha512-XhUjWR5CFaQ03JOP+iSDS9koy8T5jfoImCZ4XprElw3BXsSk4MpVYOLw/6LTDKZhO13PlAXnB5gS4MHQTpkSOw== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -19432,24 +19262,6 @@ markdown-to-jsx@^7.1.3: resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.3.tgz#f00bae66c0abe7dd2d274123f84cb6bd2a2c7c6a" integrity sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w== -material-table@^1.69.1: - version "1.69.3" - resolved "https://registry.npmjs.org/material-table/-/material-table-1.69.3.tgz#3f282cffecf5461985965cd4a516bebb8069bf2d" - integrity sha512-UT/eQbUqfAg6XstcnM8dOQNgWmWbH5tbRmwqfGfPKQUQQXq/jb1z2spFHXPJBhEZFmk+Q95HlopiE6nAHymLMw== - dependencies: - "@date-io/date-fns" "1.1.0" - "@material-ui/pickers" "3.2.2" - classnames "2.2.6" - date-fns "2.0.0-alpha.27" - debounce "1.2.0" - fast-deep-equal "2.0.1" - filefy "0.1.10" - jspdf "2.1.0" - jspdf-autotable "3.5.9" - prop-types "15.6.2" - react-beautiful-dnd "13.0.0" - react-double-scrollbar "0.0.15" - math-expression-evaluator@^1.2.14: version "1.2.22" resolved "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e" @@ -20351,7 +20163,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -23147,14 +22958,6 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.6.2: - version "15.6.2" - resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" - integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== - dependencies: - loose-envify "^1.3.1" - object-assign "^4.1.1" - prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -23402,7 +23205,7 @@ raf-schd@^4.0.2: resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@^3.4.0, raf@^3.4.1: +raf@^3.4.0: version "3.4.1" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -23495,7 +23298,7 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-beautiful-dnd@13.0.0, react-beautiful-dnd@^13.0.0: +react-beautiful-dnd@^13.0.0: version "13.0.0" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== @@ -24848,11 +24651,6 @@ rgba-regex@^1.0.0: resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rgbcolor@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" - integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0= - rifm@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" @@ -25969,11 +25767,6 @@ stack-utils@^2.0.2: dependencies: escape-string-regexp "^2.0.0" -stackblur-canvas@^2.0.0: - version "2.5.0" - resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz#aa87bbed1560fdcd3138fff344fc6a1c413ebac4" - integrity sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ== - stackframe@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.1.1.tgz#ffef0a3318b1b60c3b58564989aca5660729ec71" @@ -26552,11 +26345,6 @@ svg-parser@^2.0.2: resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svg-pathdata@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" - integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== - svgo@^1.0.0, svgo@^1.2.2: version "1.3.2" resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" @@ -26948,13 +26736,6 @@ text-hex@1.0.x: resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -text-segmentation@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz#1f828fa14aa101c114ded1bda35ba7dcc17c9858" - integrity sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw== - dependencies: - utrie "^1.0.1" - text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -28143,13 +27924,6 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -utrie@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz#e155235ebcbddc89ae09261ab6e773ce61401b2f" - integrity sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg== - dependencies: - base64-arraybuffer "^1.0.1" - uuid-browser@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410" From a15d02851747791d6e2fde8f82d18b12cb990b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 Oct 2021 16:07:59 +0200 Subject: [PATCH 141/245] more api fixes, in integration and theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/lazy-dodos-drum.md | 6 + packages/integration/api-report.md | 212 +++++------------- packages/integration/src/ScmIntegrations.ts | 14 +- .../integration/src/awsS3/AwsS3Integration.ts | 6 + packages/integration/src/awsS3/config.ts | 13 +- .../integration/src/azure/AzureIntegration.ts | 5 + packages/integration/src/azure/config.ts | 8 +- packages/integration/src/azure/core.ts | 20 +- .../src/bitbucket/BitbucketIntegration.ts | 5 + packages/integration/src/bitbucket/config.ts | 8 +- packages/integration/src/bitbucket/core.ts | 20 +- .../src/github/GitHubIntegration.test.ts | 15 +- .../src/github/GitHubIntegration.ts | 18 +- .../src/github/GithubCredentialsProvider.ts | 46 +++- packages/integration/src/github/config.ts | 15 +- packages/integration/src/github/core.ts | 10 +- packages/integration/src/github/index.ts | 12 +- .../src/gitlab/GitLabIntegration.ts | 5 + packages/integration/src/gitlab/config.ts | 8 +- packages/integration/src/gitlab/core.ts | 10 +- packages/integration/src/googleGcs/config.ts | 5 +- packages/integration/src/helpers.ts | 6 +- packages/integration/src/index.ts | 7 +- packages/integration/src/registry.ts | 10 +- packages/integration/src/types.ts | 21 +- packages/theme/api-report.md | 61 ++++- packages/theme/src/types.ts | 12 +- 27 files changed, 342 insertions(+), 236 deletions(-) create mode 100644 .changeset/lazy-dodos-drum.md diff --git a/.changeset/lazy-dodos-drum.md b/.changeset/lazy-dodos-drum.md new file mode 100644 index 0000000000..81852ac7e5 --- /dev/null +++ b/.changeset/lazy-dodos-drum.md @@ -0,0 +1,6 @@ +--- +'@backstage/integration': patch +'@backstage/theme': patch +--- + +More API fixes: mark things public, add docs, fix exports diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 01f84383fe..48a00e633a 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -6,15 +6,11 @@ import { Config } from '@backstage/config'; import { RestEndpointMethodTypes } from '@octokit/rest'; -// Warning: (ae-missing-release-tag) "AwsS3Integration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class AwsS3Integration implements ScmIntegration { constructor(integrationConfig: AwsS3IntegrationConfig); // (undocumented) get config(): AwsS3IntegrationConfig; - // Warning: (ae-forgotten-export) The symbol "ScmIntegrationsFactory" needs to be exported by the entry point index.d.ts - // // (undocumented) static factory: ScmIntegrationsFactory; // (undocumented) @@ -31,8 +27,6 @@ export class AwsS3Integration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "AwsS3IntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AwsS3IntegrationConfig = { host: string; @@ -41,9 +35,7 @@ export type AwsS3IntegrationConfig = { roleArn?: string; }; -// Warning: (ae-missing-release-tag) "AzureIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class AzureIntegration implements ScmIntegration { constructor(integrationConfig: AzureIntegrationConfig); // (undocumented) @@ -64,17 +56,13 @@ export class AzureIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "AzureIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AzureIntegrationConfig = { host: string; token?: string; }; -// Warning: (ae-missing-release-tag) "BitbucketIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class BitbucketIntegration implements ScmIntegration { constructor(integrationConfig: BitbucketIntegrationConfig); // (undocumented) @@ -95,8 +83,6 @@ export class BitbucketIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "BitbucketIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type BitbucketIntegrationConfig = { host: string; @@ -106,8 +92,6 @@ export type BitbucketIntegrationConfig = { appPassword?: string; }; -// Warning: (ae-missing-release-tag) "defaultScmResolveUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function defaultScmResolveUrl(options: { url: string; @@ -115,92 +99,44 @@ export function defaultScmResolveUrl(options: { lineNumber?: number; }): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getAzureCommitsUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureCommitsUrl(url: string): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getAzureDownloadUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureDownloadUrl(url: string): string; -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getAzureFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureFileFetchUrl(url: string): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getAzureRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, ): RequestInit; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getBitbucketDefaultBranch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketDefaultBranch( url: string, config: BitbucketIntegrationConfig, ): Promise; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getBitbucketDownloadUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketDownloadUrl( url: string, config: BitbucketIntegrationConfig, ): Promise; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getBitbucketFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketFileFetchUrl( url: string, config: BitbucketIntegrationConfig, ): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getBitbucketRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, ): RequestInit; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-forgotten-export) The symbol "GithubCredentials" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "getGitHubFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getGitHubFileFetchUrl( url: string, @@ -208,36 +144,34 @@ export function getGitHubFileFetchUrl( credentials: GithubCredentials, ): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getGitHubRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export function getGitHubRequestOptions( config: GitHubIntegrationConfig, credentials: GithubCredentials, ): RequestInit; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getGitLabFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getGitLabFileFetchUrl( url: string, config: GitLabIntegrationConfig, ): Promise; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getGitLabRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getGitLabRequestOptions( config: GitLabIntegrationConfig, ): RequestInit; -// Warning: (ae-missing-release-tag) "GithubAppCredentialsMux" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type GithubAppConfig = { + appId: number; + privateKey: string; + webhookSecret: string; + clientId: string; + clientSecret: string; + allowedInstallationOwners?: string[]; +}; + +// @public export class GithubAppCredentialsMux { constructor(config: GitHubIntegrationConfig); // (undocumented) @@ -248,33 +182,26 @@ export class GithubAppCredentialsMux { getAppToken(owner: string, repo?: string): Promise; } -// Warning: (ae-missing-release-tag) "GithubCredentialsProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type GithubCredentials = { + headers?: { + [name: string]: string; + }; + token?: string; + type: GithubCredentialType; +}; + +// @public export class GithubCredentialsProvider { // (undocumented) static create(config: GitHubIntegrationConfig): GithubCredentialsProvider; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" getCredentials(opts: { url: string }): Promise; } -// Warning: (ae-missing-release-tag) "GithubCredentialType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type GithubCredentialType = 'app' | 'token'; -// Warning: (ae-missing-release-tag) "GitHubIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class GitHubIntegration implements ScmIntegration { constructor(integrationConfig: GitHubIntegrationConfig); // (undocumented) @@ -295,8 +222,6 @@ export class GitHubIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "GitHubIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GitHubIntegrationConfig = { host: string; @@ -306,9 +231,7 @@ export type GitHubIntegrationConfig = { apps?: GithubAppConfig[]; }; -// Warning: (ae-missing-release-tag) "GitLabIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class GitLabIntegration implements ScmIntegration { constructor(integrationConfig: GitLabIntegrationConfig); // (undocumented) @@ -329,8 +252,6 @@ export class GitLabIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "GitLabIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GitLabIntegrationConfig = { host: string; @@ -339,114 +260,89 @@ export type GitLabIntegrationConfig = { baseUrl: string; }; -// Warning: (ae-missing-release-tag) "GoogleGcsIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GoogleGcsIntegrationConfig = { clientEmail?: string; privateKey?: string; }; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readAwsS3IntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export interface IntegrationsByType { + // (undocumented) + awsS3: ScmIntegrationsGroup; + // (undocumented) + azure: ScmIntegrationsGroup; + // (undocumented) + bitbucket: ScmIntegrationsGroup; + // (undocumented) + github: ScmIntegrationsGroup; + // (undocumented) + gitlab: ScmIntegrationsGroup; +} + // @public export function readAwsS3IntegrationConfig( config: Config, ): AwsS3IntegrationConfig; -// Warning: (ae-missing-release-tag) "readAwsS3IntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function readAwsS3IntegrationConfigs( configs: Config[], ): AwsS3IntegrationConfig[]; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readAzureIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readAzureIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readAzureIntegrationConfigs( configs: Config[], ): AzureIntegrationConfig[]; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readBitbucketIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readBitbucketIntegrationConfig( config: Config, ): BitbucketIntegrationConfig; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readBitbucketIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readBitbucketIntegrationConfigs( configs: Config[], ): BitbucketIntegrationConfig[]; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readGitHubIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitHubIntegrationConfig( config: Config, ): GitHubIntegrationConfig; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readGitHubIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitHubIntegrationConfigs( configs: Config[], ): GitHubIntegrationConfig[]; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readGitLabIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitLabIntegrationConfig( config: Config, ): GitLabIntegrationConfig; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readGitLabIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitLabIntegrationConfigs( configs: Config[], ): GitLabIntegrationConfig[]; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "readGoogleGcsIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGoogleGcsIntegrationConfig( config: Config, ): GoogleGcsIntegrationConfig; -// Warning: (ae-missing-release-tag) "replaceUrlType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function replaceGitHubUrlType( url: string, type: 'blob' | 'tree' | 'edit', ): string; -// Warning: (ae-missing-release-tag) "ScmIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface ScmIntegration { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen resolveEditUrl(url: string): string; // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters @@ -460,8 +356,6 @@ export interface ScmIntegration { type: string; } -// Warning: (ae-missing-release-tag) "ScmIntegrationRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface ScmIntegrationRegistry extends ScmIntegrationsGroup { @@ -475,7 +369,6 @@ export interface ScmIntegrationRegistry github: ScmIntegrationsGroup; // (undocumented) gitlab: ScmIntegrationsGroup; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen resolveEditUrl(url: string): string; // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters @@ -487,11 +380,8 @@ export interface ScmIntegrationRegistry }): string; } -// Warning: (ae-missing-release-tag) "ScmIntegrations" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class ScmIntegrations implements ScmIntegrationRegistry { - // Warning: (ae-forgotten-export) The symbol "IntegrationsByType" needs to be exported by the entry point index.d.ts constructor(integrationsByType: IntegrationsByType); // (undocumented) get awsS3(): ScmIntegrationsGroup; @@ -521,20 +411,20 @@ export class ScmIntegrations implements ScmIntegrationRegistry { }): string; } -// Warning: (ae-missing-release-tag) "ScmIntegrationsGroup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type ScmIntegrationsFactory = (options: { + config: Config; +}) => ScmIntegrationsGroup; + // @public export interface ScmIntegrationsGroup { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen byHost(host: string): T | undefined; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen byUrl(url: string | URL): T | undefined; list(): T[]; } // Warnings were encountered during analysis: // -// src/github/config.d.ts:41:5 - (ae-forgotten-export) The symbol "GithubAppConfig" needs to be exported by the entry point index.d.ts -// src/gitlab/config.d.ts:27:68 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/gitlab/config.d.ts:27:63 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/gitlab/config.d.ts:29:68 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/gitlab/config.d.ts:29:63 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" ``` diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 983ab7d24b..add9e4e016 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -24,14 +24,24 @@ import { defaultScmResolveUrl } from './helpers'; import { ScmIntegration, ScmIntegrationsGroup } from './types'; import { ScmIntegrationRegistry } from './registry'; -type IntegrationsByType = { +/** + * The set of supported integrations. + * + * @public + */ +export interface IntegrationsByType { awsS3: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; bitbucket: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; -}; +} +/** + * Exposes the set of supported integrations. + * + * @public + */ export class ScmIntegrations implements ScmIntegrationRegistry { private readonly byType: IntegrationsByType; diff --git a/packages/integration/src/awsS3/AwsS3Integration.ts b/packages/integration/src/awsS3/AwsS3Integration.ts index e987b8f7a3..1507ff514c 100644 --- a/packages/integration/src/awsS3/AwsS3Integration.ts +++ b/packages/integration/src/awsS3/AwsS3Integration.ts @@ -18,6 +18,11 @@ import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AwsS3IntegrationConfig, readAwsS3IntegrationConfigs } from './config'; +/** + * Integrates with AWS S3 or compatible solutions. + * + * @public + */ export class AwsS3Integration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readAwsS3IntegrationConfigs( @@ -42,6 +47,7 @@ export class AwsS3Integration implements ScmIntegration { } constructor(private readonly integrationConfig: AwsS3IntegrationConfig) {} + resolveUrl(options: { url: string; base: string; diff --git a/packages/integration/src/awsS3/config.ts b/packages/integration/src/awsS3/config.ts index 5f9cd46a22..6b5a23c85c 100644 --- a/packages/integration/src/awsS3/config.ts +++ b/packages/integration/src/awsS3/config.ts @@ -21,8 +21,9 @@ const AMAZON_AWS_HOST = 'amazonaws.com'; /** * The configuration parameters for a single AWS S3 provider. + * + * @public */ - export type AwsS3IntegrationConfig = { /** * The host of the target that this matches on, e.g. "amazonaws.com" @@ -50,7 +51,8 @@ export type AwsS3IntegrationConfig = { /** * Reads a single Aws S3 integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readAwsS3IntegrationConfig( @@ -70,6 +72,13 @@ export function readAwsS3IntegrationConfig( return { host, accessKeyId, secretAccessKey, roleArn }; } +/** + * Reads a set of AWS S3 integration configs, and inserts some defaults for + * public Amazon AWS if not specified. + * + * @param configs - The config objects of the integrations + * @public + */ export function readAwsS3IntegrationConfigs( configs: Config[], ): AwsS3IntegrationConfig[] { diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index ab1b2489ae..d036d88c21 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -19,6 +19,11 @@ import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AzureUrl } from './AzureUrl'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; +/** + * Microsoft Azure based integration. + * + * @public + */ export class AzureIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readAzureIntegrationConfigs( diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 9755bbbe24..2d722d1589 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -21,6 +21,8 @@ const AZURE_HOST = 'dev.azure.com'; /** * The configuration parameters for a single Azure provider. + * + * @public */ export type AzureIntegrationConfig = { /** @@ -41,7 +43,8 @@ export type AzureIntegrationConfig = { /** * Reads a single Azure integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readAzureIntegrationConfig( config: Config, @@ -62,7 +65,8 @@ export function readAzureIntegrationConfig( * Reads a set of Azure integration configs, and inserts some defaults for * public Azure if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readAzureIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 30603b1f09..7461954267 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -21,11 +21,14 @@ import { AzureIntegrationConfig } from './config'; * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * - * Converts - * from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents - * to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} + * @remarks * - * @param url A URL pointing to a file + * Converts + * - from: `https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents` + * - to: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch}` + * + * @param url - A URL pointing to a file + * @public */ export function getAzureFileFetchUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toFileUrl(); @@ -35,7 +38,8 @@ export function getAzureFileFetchUrl(url: string): string { * Given a URL pointing to a path on a provider, returns a URL that is suitable * for downloading the subtree. * - * @param url A URL pointing to a path + * @param url - A URL pointing to a path + * @public */ export function getAzureDownloadUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toArchiveUrl(); @@ -44,7 +48,8 @@ export function getAzureDownloadUrl(url: string): string { /** * Given a URL, return the API URL to fetch commits on the branch. * - * @param url A URL pointing to a repository or a sub-path + * @param url - A URL pointing to a repository or a sub-path + * @public */ export function getAzureCommitsUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toCommitsUrl(); @@ -53,7 +58,8 @@ export function getAzureCommitsUrl(url: string): string { /** * Gets the request options necessary to make requests to a given provider. * - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getAzureRequestOptions( config: AzureIntegrationConfig, diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index 07c04cd41e..6c21d716b7 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -22,6 +22,11 @@ import { readBitbucketIntegrationConfigs, } from './config'; +/** + * A Bitbucket based integration. + * + * @public + */ export class BitbucketIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config, diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index e2758952e9..1cd911aad3 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -23,6 +23,8 @@ const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0'; /** * The configuration parameters for a single Bitbucket API provider. + * + * @public */ export type BitbucketIntegrationConfig = { /** @@ -66,7 +68,8 @@ export type BitbucketIntegrationConfig = { /** * Reads a single Bitbucket integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readBitbucketIntegrationConfig( config: Config, @@ -102,7 +105,8 @@ export function readBitbucketIntegrationConfig( * Reads a set of Bitbucket integration configs, and inserts some defaults for * public Bitbucket if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readBitbucketIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index f8f8f56fa8..afcb9fcfdb 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -21,8 +21,9 @@ import { BitbucketIntegrationConfig } from './config'; /** * Given a URL pointing to a path on a provider, returns the default branch. * - * @param url A URL pointing to a path - * @param config The relevant provider config + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public */ export async function getBitbucketDefaultBranch( url: string, @@ -71,8 +72,9 @@ export async function getBitbucketDefaultBranch( * Given a URL pointing to a path on a provider, returns a URL that is suitable * for downloading the subtree. * - * @param url A URL pointing to a path - * @param config The relevant provider config + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public */ export async function getBitbucketDownloadUrl( url: string, @@ -108,12 +110,15 @@ export async function getBitbucketDownloadUrl( * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * + * @remarks + * * Converts * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml * - * @param url A URL pointing to a file - * @param config The relevant provider config + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public */ export function getBitbucketFileFetchUrl( url: string, @@ -148,7 +153,8 @@ export function getBitbucketFileFetchUrl( /** * Gets the request options necessary to make requests to a given provider. * - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts index efc1de782e..ccceb34d88 100644 --- a/packages/integration/src/github/GitHubIntegration.test.ts +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { GitHubIntegration, replaceUrlType } from './GitHubIntegration'; +import { GitHubIntegration, replaceGitHubUrlType } from './GitHubIntegration'; describe('GitHubIntegration', () => { it('has a working factory', () => { @@ -80,25 +80,28 @@ describe('GitHubIntegration', () => { }); }); -describe('replaceUrlType', () => { +describe('replaceGitHubUrlType', () => { it('should replace with expected type', () => { expect( - replaceUrlType( + replaceGitHubUrlType( 'https://github.com/backstage/backstage/blob/master/README.md', 'edit', ), ).toBe('https://github.com/backstage/backstage/edit/master/README.md'); expect( - replaceUrlType( + replaceGitHubUrlType( 'https://github.com/webmodules/blob/blob/master/test', 'tree', ), ).toBe('https://github.com/webmodules/blob/tree/master/test'); expect( - replaceUrlType('https://github.com/blob/blob/blob/master/test', 'tree'), + replaceGitHubUrlType( + 'https://github.com/blob/blob/blob/master/test', + 'tree', + ), ).toBe('https://github.com/blob/blob/tree/master/test'); expect( - replaceUrlType( + replaceGitHubUrlType( 'https://github.com/backstage/backstage/edit/tree/README.md', 'blob', ), diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index 3b53fe51e8..1db0b1fae5 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -21,6 +21,11 @@ import { readGitHubIntegrationConfigs, } from './config'; +/** + * A GitHub based integration. + * + * @public + */ export class GitHubIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitHubIntegrationConfigs( @@ -54,15 +59,22 @@ export class GitHubIntegration implements ScmIntegration { // GitHub uses blob URLs for files and tree urls for directory listings. But // there is a redirect from tree to blob for files, so we can always return // tree urls here. - return replaceUrlType(defaultScmResolveUrl(options), 'tree'); + return replaceGitHubUrlType(defaultScmResolveUrl(options), 'tree'); } resolveEditUrl(url: string): string { - return replaceUrlType(url, 'edit'); + return replaceGitHubUrlType(url, 'edit'); } } -export function replaceUrlType( +/** + * Takes a GitHub URL and replaces the type part (blob, tree etc). + * + * @param url - The original URL + * @param type - The desired type, e.g. "blob" + * @public + */ +export function replaceGitHubUrlType( url: string, type: 'blob' | 'tree' | 'edit', ): string { diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index 278a1ed028..ece692fecc 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -159,7 +159,11 @@ class GithubAppManager { } } -// GithubAppCredentialsMux corresponds to a Github installation which internally could hold several GitHub Apps. +/** + * Corresponds to a Github installation which internally could hold several GitHub Apps. + * + * @public + */ export class GithubAppCredentialsMux { private readonly apps: GithubAppManager[]; @@ -211,15 +215,32 @@ export class GithubAppCredentialsMux { } } +/** + * The type of credentials produced by the credential provider. + * + * @public + */ export type GithubCredentialType = 'app' | 'token'; +/** + * A set of credentials information for a GitHub integration. + * + * @public + */ export type GithubCredentials = { headers?: { [name: string]: string }; token?: string; type: GithubCredentialType; }; -// TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake +/** + * Handles the creation and caching of credentials for GitHub integrations. + * + * @public + * @remarks + * + * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake + */ export class GithubCredentialsProvider { static create(config: GitHubIntegrationConfig): GithubCredentialsProvider { return new GithubCredentialsProvider( @@ -234,13 +255,24 @@ export class GithubCredentialsProvider { ) {} /** - * Returns GithubCredentials for requested url. - * Consecutive calls to this method with the same url will return cached credentials. + * Returns {@link GithubCredentials} for a given URL. + * + * @remarks + * + * Consecutive calls to this method with the same URL will return cached + * credentials. + * * The shortest lifetime for a token returned is 10 minutes. - * @param opts containing the organization or repository url - * @returns {Promise} of @type {GithubCredentials}. + * * @example - * const { token, headers } = await getCredentials({url: 'github.com/backstage/foobar'}) + * ```ts + * const { token, headers } = await getCredentials({ + * url: 'github.com/backstage/foobar' + * }) + * ``` + * + * @param opts - The organization or repository URL + * @returns A promise of {@link GithubCredentials}. */ async getCredentials(opts: { url: string }): Promise { const parsed = parseGitUrl(opts.url); diff --git a/packages/integration/src/github/config.ts b/packages/integration/src/github/config.ts index 49674681f1..93801e96fb 100644 --- a/packages/integration/src/github/config.ts +++ b/packages/integration/src/github/config.ts @@ -24,6 +24,8 @@ const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com'; /** * The configuration parameters for a single GitHub integration. + * + * @public */ export type GitHubIntegrationConfig = { /** @@ -70,7 +72,12 @@ export type GitHubIntegrationConfig = { /** * The configuration parameters for authenticating a GitHub Application. - * A Github Apps configuration can be generated using the `backstage-cli create-github-app` command. + * + * @remarks + * + * A GitHub Apps configuration can be generated using the `backstage-cli create-github-app` command. + * + * @public */ export type GithubAppConfig = { /** @@ -107,7 +114,8 @@ export type GithubAppConfig = { /** * Reads a single GitHub integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readGitHubIntegrationConfig( config: Config, @@ -152,7 +160,8 @@ export function readGitHubIntegrationConfig( * Reads a set of GitHub integration configs, and inserts some defaults for * public GitHub if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readGitHubIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index 49695ec5ac..2c5e739a26 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -22,13 +22,16 @@ import { GithubCredentials } from './GithubCredentialsProvider'; * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * + * @remarks + * * Converts * from: https://github.com/a/b/blob/branchname/path/to/c.yaml * to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname * or: https://raw.githubusercontent.com/a/b/branchname/c.yaml * - * @param url A URL pointing to a file - * @param config The relevant provider config + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public */ export function getGitHubFileFetchUrl( url: string, @@ -64,7 +67,8 @@ export function getGitHubFileFetchUrl( * Gets the request options necessary to make requests to a given provider. * * @deprecated This function is no longer used internally - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getGitHubRequestOptions( config: GitHubIntegrationConfig, diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 80d9ce2f26..9c13f13135 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -18,14 +18,14 @@ export { readGitHubIntegrationConfig, readGitHubIntegrationConfigs, } from './config'; -export type { GitHubIntegrationConfig } from './config'; +export type { GithubAppConfig, GitHubIntegrationConfig } from './config'; export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; export { GithubAppCredentialsMux, GithubCredentialsProvider, } from './GithubCredentialsProvider'; -export type { GithubCredentialType } from './GithubCredentialsProvider'; -export { - GitHubIntegration, - replaceUrlType as replaceGitHubUrlType, -} from './GitHubIntegration'; +export type { + GithubCredentials, + GithubCredentialType, +} from './GithubCredentialsProvider'; +export { GitHubIntegration, replaceGitHubUrlType } from './GitHubIntegration'; diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 70a7c11987..cb24829946 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -21,6 +21,11 @@ import { readGitLabIntegrationConfigs, } from './config'; +/** + * A GitLab based integration. + * + * @public + */ export class GitLabIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitLabIntegrationConfigs( diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 7471ba0078..f9c15c3219 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -23,6 +23,8 @@ const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; /** * The configuration parameters for a single GitLab integration. + * + * @public */ export type GitLabIntegrationConfig = { /** @@ -57,7 +59,8 @@ export type GitLabIntegrationConfig = { /** * Reads a single GitLab integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readGitLabIntegrationConfig( config: Config, @@ -104,7 +107,8 @@ export function readGitLabIntegrationConfig( * Reads a set of GitLab integration configs, and inserts some defaults for * public GitLab if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readGitLabIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index b30d44e2db..e6f4dbc910 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -21,6 +21,8 @@ import fetch from 'cross-fetch'; * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * + * @remarks + * * Converts * from: https://gitlab.example.com/a/b/blob/master/c.yaml * to: https://gitlab.example.com/a/b/raw/master/c.yaml @@ -28,8 +30,9 @@ import fetch from 'cross-fetch'; * from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath * to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch * - * @param url A URL pointing to a file - * @param config The relevant provider config + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public */ export async function getGitLabFileFetchUrl( url: string, @@ -48,7 +51,8 @@ export async function getGitLabFileFetchUrl( /** * Gets the request options necessary to make requests to a given provider. * - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getGitLabRequestOptions( config: GitLabIntegrationConfig, diff --git a/packages/integration/src/googleGcs/config.ts b/packages/integration/src/googleGcs/config.ts index 0945a0e467..3c03204e4c 100644 --- a/packages/integration/src/googleGcs/config.ts +++ b/packages/integration/src/googleGcs/config.ts @@ -18,6 +18,8 @@ import { Config } from '@backstage/config'; /** * The configuration parameters for a single Google Cloud Storage provider. + * + * @public */ export type GoogleGcsIntegrationConfig = { /** @@ -33,7 +35,8 @@ export type GoogleGcsIntegrationConfig = { /** * Reads a single Google GCS integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readGoogleGcsIntegrationConfig( config: Config, diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index 52d2cf6571..2e5552d55c 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -59,8 +59,10 @@ export function basicIntegrations( } /** - * Default implementation of ScmIntegration.resolveUrl, that only works with - * URL pathname based providers. + * Default implementation of {@link ScmIntegration} `resolveUrl`, that only + * works with URL pathname based providers. + * + * @public */ export function defaultScmResolveUrl(options: { url: string; diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index 3d5f336e8d..2f33b76ab3 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -28,5 +28,10 @@ export * from './googleGcs'; export * from './awsS3'; export { defaultScmResolveUrl } from './helpers'; export { ScmIntegrations } from './ScmIntegrations'; -export type { ScmIntegration, ScmIntegrationsGroup } from './types'; +export type { IntegrationsByType } from './ScmIntegrations'; +export type { + ScmIntegration, + ScmIntegrationsFactory, + ScmIntegrationsGroup, +} from './types'; export type { ScmIntegrationRegistry } from './registry'; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 21ca472ccb..038594d259 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -23,6 +23,8 @@ import { GitLabIntegration } from './gitlab/GitLabIntegration'; /** * Holds all registered SCM integrations, of all types. + * + * @public */ export interface ScmIntegrationRegistry extends ScmIntegrationsGroup { @@ -44,9 +46,9 @@ export interface ScmIntegrationRegistry * not resolve to `https://hostname/b.yaml` but rather to * `/b.yaml` inside the file tree of that same repo. * - * @param options.url The (absolute or relative) URL or path to resolve - * @param options.base The base URL onto which this resolution happens - * @param options.lineNumber The line number in the target file to link to, starting with 1. Only applicable when linking to files. + * @param options.url - The (absolute or relative) URL or path to resolve + * @param options.base - The base URL onto which this resolution happens + * @param options.lineNumber - The line number in the target file to link to, starting with 1. Only applicable when linking to files. */ resolveUrl(options: { url: string; @@ -63,7 +65,7 @@ export interface ScmIntegrationRegistry * If this is not possible, the integration can fall back to a URL to view * the file in the web interface. * - * @param url The absolute URL to the file that should be edited. + * @param url - The absolute URL to the file that should be edited. */ resolveEditUrl(url: string): string; } diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index e66b20f8f8..5b17edfb3d 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -18,6 +18,8 @@ import { Config } from '@backstage/config'; /** * Encapsulates a single SCM integration. + * + * @public */ export interface ScmIntegration { /** @@ -43,9 +45,9 @@ export interface ScmIntegration { * not resolve to `https://hostname/b.yaml` but rather to * `/b.yaml` inside the file tree of that same repo. * - * @param options.url The (absolute or relative) URL or path to resolve - * @param options.base The base URL onto which this resolution happens - * @param options.lineNumber The line number in the target file to link to, starting with 1. Only applicable when linking to files. + * @param options.url - The (absolute or relative) URL or path to resolve + * @param options.base - The base URL onto which this resolution happens + * @param options.lineNumber - The line number in the target file to link to, starting with 1. Only applicable when linking to files. */ resolveUrl(options: { url: string; @@ -62,13 +64,15 @@ export interface ScmIntegration { * If this is not possible, the integration can fall back to a URL to view * the file in the web interface. * - * @param url The absolute URL to the file that should be edited. + * @param url - The absolute URL to the file that should be edited. */ resolveEditUrl(url: string): string; } /** * Encapsulates several integrations, that are all of the same type. + * + * @public */ export interface ScmIntegrationsGroup { /** @@ -79,18 +83,23 @@ export interface ScmIntegrationsGroup { /** * Fetches an integration of this type by URL. * - * @param url A URL that matches a registered integration of this type + * @param url - A URL that matches a registered integration of this type */ byUrl(url: string | URL): T | undefined; /** * Fetches an integration of this type by host name. * - * @param url A host name that matches a registered integration of this type + * @param host - A host name that matches a registered integration of this type */ byHost(host: string): T | undefined; } +/** + * A factory function that creates an integration group based on configuration. + * + * @public + */ export type ScmIntegrationsFactory = (options: { config: Config; }) => ScmIntegrationsGroup; diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 05833e5e91..41ea2420f2 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -9,16 +9,71 @@ import { PaletteOptions } from '@material-ui/core/styles/createPalette'; import { Theme } from '@material-ui/core'; import { ThemeOptions } from '@material-ui/core'; -// Warning: (ae-forgotten-export) The symbol "PaletteAdditions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "BackstagePalette" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type BackstagePalette = Palette & PaletteAdditions; +export type BackstagePalette = Palette & BackstagePaletteAdditions; + +// @public +export type BackstagePaletteAdditions = { + status: { + ok: string; + warning: string; + error: string; + pending: string; + running: string; + aborted: string; + }; + border: string; + textContrast: string; + textVerySubtle: string; + textSubtle: string; + highlight: string; + errorBackground: string; + warningBackground: string; + infoBackground: string; + errorText: string; + infoText: string; + warningText: string; + linkHover: string; + link: string; + gold: string; + navigation: { + background: string; + indicator: string; + color: string; + selectedColor: string; + }; + tabbar: { + indicator: string; + }; + bursts: { + fontColor: string; + slackChannelText: string; + backgroundColor: { + default: string; + }; + gradient: { + linear: string; + }; + }; + pinSidebarButton: { + icon: string; + background: string; + }; + banner: { + info: string; + error: string; + text: string; + link: string; + }; +}; // Warning: (ae-missing-release-tag) "BackstagePaletteOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; +export type BackstagePaletteOptions = PaletteOptions & + BackstagePaletteAdditions; // Warning: (ae-missing-release-tag) "BackstageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 76a1b7eb5c..770b55d341 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -20,7 +20,12 @@ import { Palette, } from '@material-ui/core/styles/createPalette'; -type PaletteAdditions = { +/** + * Backstage specific additions to the material-ui palette. + * + * @public + */ +export type BackstagePaletteAdditions = { status: { ok: string; warning: string; @@ -74,8 +79,9 @@ type PaletteAdditions = { }; }; -export type BackstagePalette = Palette & PaletteAdditions; -export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; +export type BackstagePalette = Palette & BackstagePaletteAdditions; +export type BackstagePaletteOptions = PaletteOptions & + BackstagePaletteAdditions; export type PageThemeSelector = { themeId: string; From dc6d2eb2a3835c8a39e6249ce851935b58f0a1fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 14:32:56 +0000 Subject: [PATCH 142/245] build(deps): bump webpack-dev-server from 4.0.0-rc.0 to 4.3.1 Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.0.0-rc.0 to 4.3.1. - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.0.0-rc.0...v4.3.1) --- updated-dependencies: - dependency-name: webpack-dev-server dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 104 ++++++++++++-------------------------- 2 files changed, 32 insertions(+), 74 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 62a008087e..ef95b6efda 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -110,7 +110,7 @@ "typescript": "^4.0.3", "util": "^0.12.3", "webpack": "^5.48.0", - "webpack-dev-server": "4.0.0-rc.0", + "webpack-dev-server": "4.3.1", "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yml-loader": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 205e829555..908a0eca4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8582,6 +8582,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1: dependencies: type-fest "^0.11.0" +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + ansi-html@0.0.7, ansi-html@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -10965,16 +10970,16 @@ colorette@1.2.1: resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -colorette@^1.2.1, colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - -colorette@^1.4.0: +colorette@^1.2.1, colorette@^1.2.2, colorette@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== +colorette@^2.0.10: + version "2.0.16" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + colors@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -15738,12 +15743,7 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-entities@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" - integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== - -html-entities@^1.2.1: +html-entities@^1.2.0, html-entities@^1.2.1: version "1.4.0" resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== @@ -16393,11 +16393,6 @@ is-absolute-url@^2.0.0: resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -18251,11 +18246,6 @@ keyv@^4.0.0, keyv@^4.0.3: dependencies: json-buffer "3.0.1" -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - kind-of@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" @@ -19171,13 +19161,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-age-cleaner@^0.1.3: - version "0.1.3" - resolved "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -19533,14 +19516,6 @@ media-typer@0.3.0: vinyl "^2.0.1" vinyl-file "^3.0.0" -mem@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122" - integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA== - dependencies: - map-age-cleaner "^0.1.3" - mimic-fn "^3.1.0" - memfs@^3.1.2, memfs@^3.2.2: version "3.2.2" resolved "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz#5de461389d596e3f23d48bb7c2afb6161f4df40e" @@ -20074,11 +20049,6 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-fn@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" - integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== - mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -21361,11 +21331,6 @@ p-cancelable@^2.0.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-each-series@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" @@ -28249,26 +28214,26 @@ webpack-dev-middleware@^3.7.3: range-parser "^1.2.1" webpack-log "^2.0.0" -webpack-dev-middleware@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz#0abe825275720e0a339978aea5f0b03b140c1584" - integrity sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw== +webpack-dev-middleware@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.2.1.tgz#97c948144349177856a3d2d9c612cc3fee180cf1" + integrity sha512-Kx1X+36Rn9JaZcQMrJ7qN3PMAuKmEDD9ZISjUj3Cgq4A6PtwYsC4mpaKotSRYH3iOF6HsUa8viHKS59FlyVifQ== dependencies: - colorette "^1.2.2" - mem "^8.1.1" + colorette "^2.0.10" memfs "^3.2.2" mime-types "^2.1.31" range-parser "^1.2.1" - schema-utils "^3.0.0" + schema-utils "^3.1.0" -webpack-dev-server@4.0.0-rc.0: - version "4.0.0-rc.0" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.0.0-rc.0.tgz#56099f1e1e877d15a1fc28a928f38bbc600e33da" - integrity sha512-9S+MywBN/ecr8AbXNVUnmbFji8UTtzLR6M5Dgy6sB5Ti/73UgHn8TMhLaSBZBkY/cmSmWHDSwUXFs8lOeARpOw== +webpack-dev-server@4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.3.1.tgz#759d3337f0fbea297fbd1e433ab04ccfc000076b" + integrity sha512-qNXQCVYo1kYhH9pgLtm8LRNkXX3XzTfHSj/zqzaqYzGPca+Qjr+81wj1jgPMCHhIhso9WEQ+kX9z23iG9PzQ7w== dependencies: - ansi-html "^0.0.7" + ansi-html-community "^0.0.8" bonjour "^3.5.0" chokidar "^3.5.1" + colorette "^2.0.10" compression "^1.7.4" connect-history-api-fallback "^1.6.0" del "^6.0.0" @@ -28278,8 +28243,6 @@ webpack-dev-server@4.0.0-rc.0: http-proxy-middleware "^2.0.0" internal-ip "^6.2.0" ipaddr.js "^2.0.1" - is-absolute-url "^3.0.3" - killable "^1.0.1" open "^8.0.9" p-retry "^4.5.0" portfinder "^1.0.28" @@ -28290,8 +28253,8 @@ webpack-dev-server@4.0.0-rc.0: spdy "^4.0.2" strip-ansi "^7.0.0" url "^0.11.0" - webpack-dev-middleware "^5.0.0" - ws "^7.5.3" + webpack-dev-middleware "^5.2.1" + ws "^8.1.0" webpack-filter-warnings-plugin@^1.2.1: version "1.2.1" @@ -28711,7 +28674,7 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -"ws@^5.2.0 || ^6.0.0 || ^7.0.0": +"ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1: version "7.5.0" resolved "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== @@ -28723,15 +28686,10 @@ ws@^6.1.2: dependencies: async-limiter "~1.0.0" -ws@^7.2.3, ws@^7.3.1: - version "7.3.1" - resolved "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== - -ws@^7.5.3: - version "7.5.3" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== +ws@^8.1.0: + version "8.2.3" + resolved "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== xcase@^2.0.1: version "2.0.1" From d895c61ebb7b0bffa4fb7f27d01293de581bea7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 14:35:45 +0000 Subject: [PATCH 143/245] build(deps-dev): bump @types/minimist from 1.2.0 to 1.2.2 Bumps [@types/minimist](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/minimist) from 1.2.0 to 1.2.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/minimist) --- updated-dependencies: - dependency-name: "@types/minimist" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 205e829555..dc22fd0d55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7241,9 +7241,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/minimist@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" - integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + version "1.2.2" + resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/minipass@*": version "2.2.0" From 38e3fbe655da2c893abe06aa744ab2d65bced21a Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 16:36:19 +0200 Subject: [PATCH 144/245] chore: bump old space size again? Signed-off-by: blam --- .github/workflows/e2e-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 73ec0c782d..acaf4e189e 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -21,7 +21,7 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=8192 name: Node ${{ matrix.node-version }} on ${{ matrix.os }} steps: From 8eedc159ae5c96cbb8b23a792e4dbe1a08c12250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 Oct 2021 16:55:36 +0200 Subject: [PATCH 145/245] fix up errors too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/lazy-dodos-drum.md | 1 + packages/errors/api-report.md | 1 - packages/errors/src/errors/CustomErrorBase.ts | 3 +++ packages/integration/api-report.md | 6 ------ packages/integration/src/registry.ts | 13 +++++++++---- packages/integration/src/types.ts | 13 +++++++++---- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.changeset/lazy-dodos-drum.md b/.changeset/lazy-dodos-drum.md index 81852ac7e5..b4d72aca28 100644 --- a/.changeset/lazy-dodos-drum.md +++ b/.changeset/lazy-dodos-drum.md @@ -1,4 +1,5 @@ --- +'@backstage/errors': patch '@backstage/integration': patch '@backstage/theme': patch --- diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index a96132cee7..4984919e6a 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -17,7 +17,6 @@ export class ConflictError extends CustomErrorBase {} // @public export class CustomErrorBase extends Error { constructor(message?: string, cause?: Error | unknown); - // (undocumented) readonly cause?: Error | undefined; } diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts index 17149cd398..786e10ef23 100644 --- a/packages/errors/src/errors/CustomErrorBase.ts +++ b/packages/errors/src/errors/CustomErrorBase.ts @@ -33,6 +33,9 @@ import { isError } from './assertion'; * ``` */ export class CustomErrorBase extends Error { + /** + * An inner error that caused this error to be thrown, if any. + */ readonly cause?: Error | undefined; constructor(message?: string, cause?: Error | unknown) { diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 48a00e633a..4c2258779a 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -344,9 +344,6 @@ export function replaceGitHubUrlType( // @public export interface ScmIntegration { resolveEditUrl(url: string): string; - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters resolveUrl(options: { url: string; base: string; @@ -370,9 +367,6 @@ export interface ScmIntegrationRegistry // (undocumented) gitlab: ScmIntegrationsGroup; resolveEditUrl(url: string): string; - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters resolveUrl(options: { url: string; base: string; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 038594d259..5864695e25 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -45,14 +45,19 @@ export interface ScmIntegrationRegistry * within the file tree of a certain repo, an absolute path of `/b.yaml` does * not resolve to `https://hostname/b.yaml` but rather to * `/b.yaml` inside the file tree of that same repo. - * - * @param options.url - The (absolute or relative) URL or path to resolve - * @param options.base - The base URL onto which this resolution happens - * @param options.lineNumber - The line number in the target file to link to, starting with 1. Only applicable when linking to files. */ resolveUrl(options: { + /** + * The (absolute or relative) URL or path to resolve. + */ url: string; + /** + * The base URL onto which this resolution happens + */ base: string; + /** + * The line number in the target file to link to, starting with 1. Only applicable when linking to files. + */ lineNumber?: number; }): string; diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index 5b17edfb3d..cb4b78eb7c 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -44,14 +44,19 @@ export interface ScmIntegration { * within the file tree of a certain repo, an absolute path of `/b.yaml` does * not resolve to `https://hostname/b.yaml` but rather to * `/b.yaml` inside the file tree of that same repo. - * - * @param options.url - The (absolute or relative) URL or path to resolve - * @param options.base - The base URL onto which this resolution happens - * @param options.lineNumber - The line number in the target file to link to, starting with 1. Only applicable when linking to files. */ resolveUrl(options: { + /** + * The (absolute or relative) URL or path to resolve + */ url: string; + /** + * The base URL onto which this resolution happens + */ base: string; + /** + * The line number in the target file to link to, starting with 1. Only applicable when linking to files. + */ lineNumber?: number; }): string; From 034031ec6879bd84f35f574b12c6a3c56294ea9e Mon Sep 17 00:00:00 2001 From: Andrew Ellis Date: Mon, 25 Oct 2021 15:07:52 -0600 Subject: [PATCH 146/245] chore(deps): bump msw to v0.35.0 Signed-off-by: Andrew Ellis --- .changeset/lazy-pandas-ring.md | 5 ++ packages/test-utils/package.json | 2 +- yarn.lock | 95 +++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 .changeset/lazy-pandas-ring.md diff --git a/.changeset/lazy-pandas-ring.md b/.changeset/lazy-pandas-ring.md new file mode 100644 index 0000000000..55f7ac5507 --- /dev/null +++ b/.changeset/lazy-pandas-ring.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 624fdf28b3..88dd54b51c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -38,7 +38,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/react": "*", - "msw": "^0.29.0", + "msw": "^0.35.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-router": "6.0.0-beta.0", diff --git a/yarn.lock b/yarn.lock index 80b35d32ae..356657f775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4601,7 +4601,7 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@mswjs/cookies@^0.1.5": +"@mswjs/cookies@^0.1.5", "@mswjs/cookies@^0.1.6": version "0.1.6" resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.1.6.tgz#176f77034ab6d7373ae5c94bcbac36fee8869249" integrity sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA== @@ -4620,6 +4620,18 @@ strict-event-emitter "^0.2.0" xmldom "^0.6.0" +"@mswjs/interceptors@^0.12.6": + version "0.12.7" + resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" + integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg== + dependencies: + "@open-draft/until" "^1.0.3" + "@xmldom/xmldom" "^0.7.2" + debug "^4.3.2" + headers-utils "^3.0.2" + outvariant "^1.2.0" + strict-event-emitter "^0.2.0" + "@n1ru4l/push-pull-async-iterable-iterator@^2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-2.1.4.tgz#a90225474352f9f159bff979905f707b9c6bcf04" @@ -6677,6 +6689,11 @@ resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cookiejar@*": version "2.1.1" resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" @@ -7005,6 +7022,14 @@ "@types/through" "*" rxjs "^6.4.0" +"@types/inquirer@^7.3.3": + version "7.3.3" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" + integrity sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -8298,7 +8323,7 @@ dependencies: tslib "^1.9.3" -"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.5": +"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.2", "@xmldom/xmldom@^0.7.5": version "0.7.5" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== @@ -15370,6 +15395,11 @@ graphql@^15.3.0, graphql@^15.4.0: resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.1.tgz#f2f84415d8985e7b84731e7f3536f8bb9d383aad" integrity sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw== +graphql@^15.5.1: + version "15.6.1" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.6.1.tgz#9125bdf057553525da251e19e96dab3d3855ddfc" + integrity sha512-3i5lu0z6dRvJ48QP9kFxBkJ7h4Kso7PS8eahyTFz5Jm6CvQfLtNIE8LX9N6JLnXTuwR+sIYnXzaWp6anOg0QQw== + grouped-queue@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/grouped-queue/-/grouped-queue-2.0.0.tgz#a2c6713f2171e45db2c300a3a9d7c119d694dac8" @@ -16314,6 +16344,26 @@ inquirer@^8.1.0: strip-ansi "^6.0.0" through "^2.3.6" +inquirer@^8.1.1: + version "8.2.0" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.2.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + internal-ip@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz#d5541e79716e406b74ac6b07b856ef18dc1621c1" @@ -16745,6 +16795,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-node-process@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" + integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + is-npm@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" @@ -20366,6 +20421,32 @@ msw@^0.29.0: type-fest "^1.1.3" yargs "^17.0.1" +msw@^0.35.0: + version "0.35.0" + resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38" + integrity sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w== + dependencies: + "@mswjs/cookies" "^0.1.6" + "@mswjs/interceptors" "^0.12.6" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/inquirer" "^7.3.3" + "@types/js-levenshtein" "^1.1.0" + chalk "^4.1.1" + chokidar "^3.4.2" + cookie "^0.4.1" + graphql "^15.5.1" + headers-utils "^3.0.2" + inquirer "^8.1.1" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.1" + node-match-path "^0.6.3" + statuses "^2.0.0" + strict-event-emitter "^0.2.0" + type-fest "^1.2.2" + yargs "^17.0.1" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -21309,6 +21390,11 @@ outdent@^0.5.0: resolved "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== +outvariant@^1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.2.1.tgz#e630f6cdc1dbf398ed857e36f219de4a005ccd35" + integrity sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA== + overlayscrollbars@^1.13.1: version "1.13.1" resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz#0b840a88737f43a946b9d87875a2f9e421d0338a" @@ -27251,6 +27337,11 @@ type-fest@^1.1.3: resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.2.1.tgz#232990aa513f3f5223abf54363975dfe3a121a2e" integrity sha512-SbmIRuXhJs8KTneu77Ecylt9zuqL683tuiLYpTRil4H++eIhqCmx6ko6KAFem9dty8sOdnEiX7j4K1nRE628fQ== +type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type-is@^1.6.16, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" From d465f2e0afd1850ca4e4b6029ad97e59721704f5 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 10:57:13 +0200 Subject: [PATCH 147/245] chore: bump the msw package and remove it from test-utils as it's not even needed Signed-off-by: blam --- packages/backend-common/package.json | 2 +- packages/catalog-client/package.json | 2 +- .../default-backend-plugin/package.json.hbs | 2 +- .../templates/default-plugin/package.json.hbs | 2 +- packages/core-app-api/package.json | 2 +- packages/core-plugin-api/package.json | 2 +- packages/integration-react/package.json | 2 +- packages/integration/package.json | 2 +- packages/test-utils/package.json | 1 - plugins/allure/package.json | 2 +- plugins/analytics-module-ga/package.json | 2 +- plugins/api-docs/package.json | 2 +- plugins/app-backend/package.json | 2 +- plugins/auth-backend/package.json | 2 +- plugins/azure-devops-backend/package.json | 2 +- plugins/azure-devops/package.json | 2 +- plugins/badges/package.json | 2 +- plugins/bitrise/package.json | 2 +- .../package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/catalog-graphql/package.json | 2 +- plugins/catalog-import/package.json | 2 +- plugins/circleci/package.json | 2 +- plugins/cloudbuild/package.json | 2 +- plugins/code-coverage-backend/package.json | 2 +- plugins/code-coverage/package.json | 2 +- plugins/config-schema/package.json | 2 +- plugins/cost-insights/package.json | 2 +- plugins/explore-react/package.json | 2 +- plugins/explore/package.json | 2 +- plugins/firehydrant/package.json | 2 +- plugins/fossa/package.json | 2 +- plugins/gcp-projects/package.json | 2 +- plugins/git-release-manager/package.json | 2 +- plugins/github-actions/package.json | 2 +- plugins/github-deployments/package.json | 2 +- plugins/gitops-profiles/package.json | 2 +- plugins/graphiql/package.json | 2 +- plugins/graphql-backend/package.json | 2 +- plugins/home/package.json | 2 +- plugins/ilert/package.json | 2 +- plugins/jenkins-backend/package.json | 2 +- plugins/jenkins/package.json | 2 +- plugins/kafka/package.json | 2 +- plugins/kubernetes/package.json | 2 +- plugins/lighthouse/package.json | 2 +- plugins/newrelic/package.json | 2 +- plugins/org/package.json | 2 +- plugins/pagerduty/package.json | 2 +- plugins/rollbar/package.json | 2 +- .../package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder/package.json | 2 +- plugins/search/package.json | 2 +- plugins/sentry/package.json | 2 +- plugins/shortcuts/package.json | 2 +- plugins/sonarqube/package.json | 2 +- plugins/splunk-on-call/package.json | 2 +- plugins/tech-radar/package.json | 2 +- plugins/techdocs-backend/package.json | 2 +- plugins/techdocs/package.json | 2 +- plugins/todo-backend/package.json | 2 +- plugins/todo/package.json | 2 +- plugins/user-settings/package.json | 2 +- plugins/xcmetrics/package.json | 2 +- yarn.lock | 79 +------------------ 66 files changed, 68 insertions(+), 140 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 60824f0c40..18a1361c98 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -99,7 +99,7 @@ "http-errors": "^1.7.3", "jest": "^26.0.1", "mock-fs": "^5.1.0", - "msw": "^0.29.0", + "msw": "^0.35.0", "mysql2": "^2.2.5", "recursive-readdir": "^2.2.2", "supertest": "^6.1.3" diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index f8dfd59e41..d4ba2c20f6 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 43b9e83fe1..5cca7dcb07 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -36,7 +36,7 @@ "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", "supertest": "{{versionQuery 'supertest' '4.0.2'}}", - "msw": "{{versionQuery 'msw' '0.29.0'}}" + "msw": "{{versionQuery 'msw' '0.35.0'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index ef56d55343..41376821ba 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -44,7 +44,7 @@ "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '13.1.8'}}", "@types/jest": "{{versionQuery '@types/jest' '26.0.7'}}", "@types/node": "{{versionQuery '@types/node' '14.14.32'}}", - "msw": "{{versionQuery 'msw' '0.29.0'}}", + "msw": "{{versionQuery 'msw' '0.35.0'}}", "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}" }, "files": [ diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index aee8eb9f02..cc79a6458e 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -56,7 +56,7 @@ "@types/node": "^14.14.32", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index c59668a432..842ca102e2 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -55,7 +55,7 @@ "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index b0867c755b..9ee94f6e9e 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -42,7 +42,7 @@ "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "msw": "^0.29.0", + "msw": "^0.35.0", "cross-fetch": "^3.0.6" }, "files": [ diff --git a/packages/integration/package.json b/packages/integration/package.json index 5dda924580..a5e48bbca4 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -44,7 +44,7 @@ "@backstage/test-utils": "^0.1.19", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 88dd54b51c..a565131c83 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -38,7 +38,6 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/react": "*", - "msw": "^0.35.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-router": "6.0.0-beta.0", diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 26a5b4cdeb..fa1acbd0a5 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -46,7 +46,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 0f9759bf4f..431ae2de52 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -44,7 +44,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 78ce0c7ed6..38acd683e9 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -64,7 +64,7 @@ "@types/node": "^14.14.32", "@types/swagger-ui-react": "^3.23.3", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 3483bfa8bd..f73e6f0b87 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -45,7 +45,7 @@ "@backstage/cli": "^0.8.0", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d593db2aa5..d9dddc073b 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -84,7 +84,7 @@ "@types/passport-saml": "^1.1.3", "@types/passport-strategy": "^0.2.35", "@types/xml2js": "^0.4.7", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index ec2bc84627..c2625f7f68 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -33,7 +33,7 @@ "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 38f3216c7b..dcae436c9f 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -54,7 +54,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 5bc2f3e519..2bf77dd00a 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -52,7 +52,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 9123188947..c5f36c2d08 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -50,7 +50,7 @@ "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index d0f664f428..68e58d922d 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -45,7 +45,7 @@ "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.19", "@types/lodash": "^4.14.151", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9a44258958..623f991bb7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -72,7 +72,7 @@ "@types/uuid": "^8.0.0", "@types/yup": "^0.29.13", "aws-sdk-mock": "^5.2.1", - "msw": "^0.29.0", + "msw": "^0.35.0", "sqlite3": "^5.0.1", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index f5705dcc76..da0c106177 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -49,7 +49,7 @@ "@graphql-codegen/typescript": "^1.17.7", "@graphql-codegen/typescript-resolvers": "^1.17.7", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index fe453ca45d..f79a3f136e 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -65,7 +65,7 @@ "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 8f49c4ac56..8a72ccf9c9 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -64,7 +64,7 @@ "@types/node": "^14.14.32", "@types/react-lazylog": "^4.5.0", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 6699fc1e00..481910954c 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -59,7 +59,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 56be697bac..53d52a9246 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -40,7 +40,7 @@ "@backstage/cli": "^0.8.0", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^4.0.2", "xml2js": "^0.4.23" }, diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 2d74ddd055..b3a87964d3 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -53,7 +53,7 @@ "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 1a6b39f0cb..7445af4efb 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -47,7 +47,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 7eedd9a5bc..922be98631 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -70,7 +70,7 @@ "@types/yup": "^0.29.13", "canvas": "^2.6.1", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 263137e93f..1dfd24dff8 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -41,7 +41,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 281d1727e8..ff2068b307 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -59,7 +59,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index e72bdfda51..dc742b5043 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -45,7 +45,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index c33e5f64ac..c8955dfff8 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -59,7 +59,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 0a3f70486e..4e91dbfdc6 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -53,7 +53,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index a63f491f48..8944745679 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -51,7 +51,7 @@ "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 7958a97cb8..6208b459ec 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -62,7 +62,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index ba226037ff..c373c91e7d 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -49,7 +49,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 9088c79fd9..6fbd0316fc 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -54,7 +54,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 1c5ebe17f6..8af52123ee 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -55,7 +55,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0", + "msw": "^0.35.0", "react-router-dom": "6.0.0-beta.0" }, "files": [ diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index 6f1f0000ea..9d90dd8782 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -50,7 +50,7 @@ "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/home/package.json b/plugins/home/package.json index 28b9fa9e66..950dc81af6 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -45,7 +45,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 34db6580fe..674b9b56ee 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -49,7 +49,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index bdae720c0e..93f5fa84d2 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -38,7 +38,7 @@ "@backstage/cli": "^0.8.0", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index fa6a5e6d52..d339eafb03 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -59,7 +59,7 @@ "@types/node": "^14.14.32", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 6b91797359..de8994ee5f 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -47,7 +47,7 @@ "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "jest-when": "^3.1.0", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 22db871ccd..673781b582 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -62,7 +62,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 8d9cf70bad..84b2e5b601 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -59,7 +59,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index c44608e2ef..f9c4c818a5 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -53,7 +53,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/org/package.json b/plugins/org/package.json index 4e89865f44..9ee26eefef 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -47,7 +47,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 35a240b5de..6b0cdaaa53 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -58,7 +58,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0", + "msw": "^0.35.0", "node-fetch": "^2.6.1" }, "files": [ diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index f35852fe74..7c9531abeb 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -61,7 +61,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 1f004b8804..fc6a79f18b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -39,7 +39,7 @@ "@types/jest": "^26.0.7", "@types/command-exists": "^1.2.0", "mock-fs": "^5.1.0", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cfea66284d..e6812c6501 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -81,7 +81,7 @@ "@types/supertest": "^2.0.8", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3", "yaml": "^1.10.0" }, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index aa4412204a..0e10fdb64c 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -77,7 +77,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/search/package.json b/plugins/search/package.json index ee8a2fb726..9d82da57f5 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -62,7 +62,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index ae3aad815f..382870d033 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -60,7 +60,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 14dfd16896..90c5addcf1 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -48,7 +48,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 971eb1e8cb..5404bb0d8b 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -59,7 +59,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 007707a735..412ef74709 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -58,7 +58,7 @@ "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0", + "msw": "^0.35.0", "node-fetch": "^2.6.1" }, "configSchema": "config.d.ts", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index bf146c4c2a..591435c737 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -58,7 +58,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index c4fec93c10..cb52d54d58 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -54,7 +54,7 @@ "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.18", "@types/dockerode": "^3.2.1", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 0aecd7d17c..69ecce1aa9 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -74,7 +74,7 @@ "@types/node": "^14.14.32", "canvas": "^2.6.1", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 67c990cf13..c3f0e469aa 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -42,7 +42,7 @@ "devDependencies": { "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/todo/package.json b/plugins/todo/package.json index d20ff43470..30053ef1a5 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -51,7 +51,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index bc836a7251..d6e665d99a 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -54,7 +54,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index c012c8e059..7ece64c095 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -47,7 +47,7 @@ "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/yarn.lock b/yarn.lock index 356657f775..6188b54b20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4601,7 +4601,7 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@mswjs/cookies@^0.1.5", "@mswjs/cookies@^0.1.6": +"@mswjs/cookies@^0.1.6": version "0.1.6" resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.1.6.tgz#176f77034ab6d7373ae5c94bcbac36fee8869249" integrity sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA== @@ -4609,17 +4609,6 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" -"@mswjs/interceptors@^0.10.0": - version "0.10.0" - resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.10.0.tgz#f5aad03c2c0591d164e3ed178b21942f1c2f8061" - integrity sha512-/M0GGpid5q2EDI+Keas1sLYF3VZFXHDE5gCmX/jHdp+OJFruVNca3PUk7A8KnGdPpuycZogdPsmRBSOXwjyA7A== - dependencies: - "@open-draft/until" "^1.0.3" - debug "^4.3.0" - headers-utils "^3.0.2" - strict-event-emitter "^0.2.0" - xmldom "^0.6.0" - "@mswjs/interceptors@^0.12.6": version "0.12.7" resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" @@ -6684,11 +6673,6 @@ dependencies: "@types/express" "*" -"@types/cookie@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" - integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== - "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" @@ -12292,7 +12276,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.2, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@4.3.2, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -15390,7 +15374,7 @@ graphql@15.5.0: resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== -graphql@^15.3.0, graphql@^15.4.0: +graphql@^15.3.0: version "15.5.1" resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.1.tgz#f2f84415d8985e7b84731e7f3536f8bb9d383aad" integrity sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw== @@ -16324,26 +16308,6 @@ inquirer@^8.0.0: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.1.0: - version "8.1.1" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.1.1.tgz#7c53d94c6d03011c7bb2a947f0dca3b98246c26a" - integrity sha512-hUDjc3vBkh/uk1gPfMAD/7Z188Q8cvTGl0nxwaCdwSbzFh6ZKkZh+s2ozVxbE5G9ZNRyeY0+lgbAIOUFsFf98w== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.3.0" - run-async "^2.4.0" - rxjs "^6.6.6" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - inquirer@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" @@ -20396,31 +20360,6 @@ msal@^1.0.2: dependencies: tslib "^1.9.3" -msw@^0.29.0: - version "0.29.0" - resolved "https://registry.npmjs.org/msw/-/msw-0.29.0.tgz#7242d575cb01db0c925241587df1fc2b79230d78" - integrity sha512-C/wz1d5uAEZRvAPAYrXG1rwLxXl0+BOs+JPrCzasoABZW3ATwS6ifSze+/DAgA93e9M86RXwvy6yDtZeZWmCFQ== - dependencies: - "@mswjs/cookies" "^0.1.5" - "@mswjs/interceptors" "^0.10.0" - "@open-draft/until" "^1.0.3" - "@types/cookie" "^0.4.0" - "@types/inquirer" "^7.3.1" - "@types/js-levenshtein" "^1.1.0" - chalk "^4.1.1" - chokidar "^3.4.2" - cookie "^0.4.1" - graphql "^15.4.0" - headers-utils "^3.0.2" - inquirer "^8.1.0" - js-levenshtein "^1.1.6" - node-fetch "^2.6.1" - node-match-path "^0.6.3" - statuses "^2.0.0" - strict-event-emitter "^0.2.0" - type-fest "^1.1.3" - yargs "^17.0.1" - msw@^0.35.0: version "0.35.0" resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38" @@ -24859,7 +24798,7 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^6.6.6, rxjs@^6.6.7: +rxjs@^6.6.7: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -27332,11 +27271,6 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.1.3: - version "1.2.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.2.1.tgz#232990aa513f3f5223abf54363975dfe3a121a2e" - integrity sha512-SbmIRuXhJs8KTneu77Ecylt9zuqL683tuiLYpTRil4H++eIhqCmx6ko6KAFem9dty8sOdnEiX7j4K1nRE628fQ== - type-fest@^1.2.2: version "1.4.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" @@ -28863,11 +28797,6 @@ xmlchars@^2.2.0: resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" - integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== - xpath@0.0.32: version "0.0.32" resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af" From c5bb1df55d549ad14b6930df1c73871ba786a180 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 10:58:25 +0200 Subject: [PATCH 148/245] chore: added changeset Signed-off-by: blam --- .changeset/moody-years-prove.md | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .changeset/moody-years-prove.md diff --git a/.changeset/moody-years-prove.md b/.changeset/moody-years-prove.md new file mode 100644 index 0000000000..9efb153b6b --- /dev/null +++ b/.changeset/moody-years-prove.md @@ -0,0 +1,68 @@ +--- +'@backstage/backend-common': patch +'@backstage/catalog-client': patch +'@backstage/cli': patch +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +'@backstage/integration': patch +'@backstage/integration-react': patch +'@backstage/test-utils': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-graphql': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-graphql-backend': patch +'@backstage/plugin-home': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-todo-backend': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-xcmetrics': patch +--- + +chore: bump `msw` dependency to `0.35.0` From 112a21ae94c047cc0d482ebfb2fb322998d83aa7 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 10:59:26 +0200 Subject: [PATCH 149/245] chore: reworking changeset Signed-off-by: blam --- .changeset/lazy-pandas-ring.md | 5 ----- .changeset/moody-years-prove.md | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .changeset/lazy-pandas-ring.md diff --git a/.changeset/lazy-pandas-ring.md b/.changeset/lazy-pandas-ring.md deleted file mode 100644 index 55f7ac5507..0000000000 --- a/.changeset/lazy-pandas-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/test-utils': patch ---- - -Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). diff --git a/.changeset/moody-years-prove.md b/.changeset/moody-years-prove.md index 9efb153b6b..be86ba05d6 100644 --- a/.changeset/moody-years-prove.md +++ b/.changeset/moody-years-prove.md @@ -65,4 +65,4 @@ '@backstage/plugin-xcmetrics': patch --- -chore: bump `msw` dependency to `0.35.0` +Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). From 640c21f0b1685c32f8580c6326b31b2c56a0107d Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Oct 2021 17:24:44 +0200 Subject: [PATCH 150/245] chore: updating packages for release Signed-off-by: blam --- .changeset/moody-years-prove.md | 62 --------------------------------- 1 file changed, 62 deletions(-) diff --git a/.changeset/moody-years-prove.md b/.changeset/moody-years-prove.md index be86ba05d6..41c45d9cc9 100644 --- a/.changeset/moody-years-prove.md +++ b/.changeset/moody-years-prove.md @@ -1,68 +1,6 @@ --- -'@backstage/backend-common': patch -'@backstage/catalog-client': patch '@backstage/cli': patch -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch -'@backstage/integration': patch -'@backstage/integration-react': patch '@backstage/test-utils': patch -'@backstage/plugin-allure': patch -'@backstage/plugin-analytics-module-ga': patch -'@backstage/plugin-api-docs': patch -'@backstage/plugin-app-backend': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-azure-devops': patch -'@backstage/plugin-azure-devops-backend': patch -'@backstage/plugin-badges': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-catalog-graphql': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-explore-react': patch -'@backstage/plugin-firehydrant': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-gcp-projects': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-gitops-profiles': patch -'@backstage/plugin-graphiql': patch -'@backstage/plugin-graphql-backend': patch -'@backstage/plugin-home': patch -'@backstage/plugin-ilert': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-newrelic': patch -'@backstage/plugin-org': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-search': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-shortcuts': patch -'@backstage/plugin-sonarqube': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-tech-radar': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/plugin-todo': patch -'@backstage/plugin-todo-backend': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-xcmetrics': patch --- Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). From f49099fc13ea584f8d6807c4b6734ebc7d08db75 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 26 Oct 2021 15:42:02 +0100 Subject: [PATCH 151/245] fix more imports to follow convention Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/src/index.ts | 7 ------- plugins/scaffolder-backend/src/scaffolder/tasks/index.ts | 4 ++++ plugins/scaffolder-backend/src/service/router.test.ts | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index 4c31e9f982..76cda63eef 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -24,10 +24,3 @@ export * from './scaffolder'; export * from './service/router'; export * from './lib/catalog'; export * from './processor'; -export { TaskAgent } from './scaffolder/tasks'; -export type { - TaskBroker, - DispatchResult, - TaskStore, - Task, -} from './scaffolder/tasks/types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index d99cbcda6e..941c27ef0e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -30,4 +30,8 @@ export type { TaskSpecV1beta3, Status, TaskEventType, + TaskBroker, + Task, + TaskStore, + DispatchResult, } from './types'; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index c0fdcb2ef7..71b6d27b5e 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -40,7 +40,7 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; -import { createRouter } from './router'; +import { createRouter } from '../index'; const createCatalogClient = (templates: any[] = []) => ({ From cfd54b9af4ce3ca1000cbcfb385cc8dfc4edbe60 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 26 Oct 2021 17:41:53 +0200 Subject: [PATCH 152/245] Fix TechDocs common bug (and tests) on windows. Signed-off-by: Eric Peterson --- .../techdocs-common/src/stages/publish/awsS3.test.ts | 9 +++------ .../src/stages/publish/googleStorage.test.ts | 7 +------ packages/techdocs-common/src/stages/publish/helpers.ts | 8 +++++--- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 9b74d89dda..1f7afa0855 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -334,15 +334,12 @@ describe('AwsS3Publish', () => { name: 'path', }; - const techDocsMetadaFilePath = path.posix.join( - ...Object.values(invalidEntityName), - 'techdocs_metadata.json', - ); - const fails = publisher.fetchTechDocsMetadata(invalidEntityName); await expect(fails).rejects.toMatchObject({ - message: `TechDocs metadata fetch failed; caused by Error: The file ${techDocsMetadaFilePath} does not exist!`, + message: expect.stringMatching( + /TechDocs metadata fetch failed; caused by Error: The file .* does not exist/i, + ), }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index d31995ea2e..c085594325 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -332,15 +332,10 @@ describe('GoogleGCSPublish', () => { name: 'path', }; - const techDocsMetadaFilePath = path.posix.join( - ...Object.values(invalidEntityName), - 'techdocs_metadata.json', - ); - const fails = publisher.fetchTechDocsMetadata(invalidEntityName); await expect(fails).rejects.toMatchObject({ - message: `The file ${techDocsMetadaFilePath} does not exist!`, + message: expect.stringMatching(/The file .* does not exist/i), }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index c23c72af38..bc7ed6b97d 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -198,10 +198,12 @@ export const getCloudPathForLocalPath = ( ? relativeFilePathTriplet : lowerCaseEntityTriplet(relativeFilePathTriplet); - const destinationWithRoot = path.join( - ...externalStorageRootPath.split(path.posix.sep), + // Again, the / delimiter is intentional, as it represents remote storage. + const destinationWithRoot = [ + // The extra filter prevents unintended double slashes and prefixes. + ...externalStorageRootPath.split(path.posix.sep).filter(s => s !== ''), destination, - ); + ].join('/'); return destinationWithRoot; // Remote storage file relative path }; From d898d29746f7fd916962af344956b3e3e89573fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mon=20=28Ray=29=20Sinnema?= Date: Tue, 26 Oct 2021 17:42:59 +0200 Subject: [PATCH 153/245] Add Adevinta Signed-off-by: Ray Sinnema --- ADOPTERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index db751bfb9b..922d2d98e7 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -58,4 +58,5 @@ | [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | | [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | | [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | -| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Adevinta](https://www.adevinta.com/) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to internal customers. | From 3e641842598b5aec809c7e15c1ca608f4cb99eb1 Mon Sep 17 00:00:00 2001 From: Ray Sinnema Date: Tue, 26 Oct 2021 18:23:17 +0200 Subject: [PATCH 154/245] Add Adevinta Signed-off-by: Ray Sinnema --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index db751bfb9b..451dc7a953 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -59,3 +59,4 @@ | [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | | [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | | [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Adevinta](https://www.adevinta.com) | [Ray Sinnemma](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | From 1a705377ffb6ce2f19307d97dcecba9d86cd60a7 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Tue, 26 Oct 2021 17:23:44 +0100 Subject: [PATCH 155/245] List custom scaffolder action packages This here adds a table with a list of opensource scaffolder actions that can be used by anyone in the community. Signed-off-by: Nicolas Arnold --- .../software-templates/writing-custom-actions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 53a90e2954..0e58644d2e 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -153,4 +153,16 @@ return await createRouter({ }); ``` +### List of custom action packages + +Here is a list of Open Source custom actions that you can add to your Backstage +scaffolder backend: + +| Name | Package | Owner | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) | +| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) | +| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) | +| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) | + Have fun! 🚀 From 10d267a1b7124c7cdeddc61abbeea37fff140a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 Oct 2021 19:36:17 +0200 Subject: [PATCH 156/245] Minor fix for the config package too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/fluffy-camels-watch.md | 5 +++++ packages/config/api-report.md | 17 ----------------- packages/config/src/reader.ts | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 .changeset/fluffy-camels-watch.md diff --git a/.changeset/fluffy-camels-watch.md b/.changeset/fluffy-camels-watch.md new file mode 100644 index 0000000000..ddbf218f97 --- /dev/null +++ b/.changeset/fluffy-camels-watch.md @@ -0,0 +1,5 @@ +--- +'@backstage/config': patch +--- + +Minor exports cleanup diff --git a/packages/config/api-report.md b/packages/config/api-report.md index 205db6ef2b..efa6b85559 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -46,39 +46,22 @@ export class ConfigReader implements Config { fallback?: ConfigReader | undefined, prefix?: string, ); - // (undocumented) static fromConfigs(configs: AppConfig[]): ConfigReader; - // (undocumented) get(key?: string): T; - // (undocumented) getBoolean(key: string): boolean; - // (undocumented) getConfig(key: string): ConfigReader; - // (undocumented) getConfigArray(key: string): ConfigReader[]; - // (undocumented) getNumber(key: string): number; - // (undocumented) getOptional(key?: string): T | undefined; - // (undocumented) getOptionalBoolean(key: string): boolean | undefined; - // (undocumented) getOptionalConfig(key: string): ConfigReader | undefined; - // (undocumented) getOptionalConfigArray(key: string): ConfigReader[] | undefined; - // (undocumented) getOptionalNumber(key: string): number | undefined; - // (undocumented) getOptionalString(key: string): string | undefined; - // (undocumented) getOptionalStringArray(key: string): string[] | undefined; - // (undocumented) getString(key: string): string; - // (undocumented) getStringArray(key: string): string[]; - // (undocumented) has(key: string): boolean; - // (undocumented) keys(): string[]; } diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index bc8a860c85..8c0c4140a5 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -72,6 +72,9 @@ export class ConfigReader implements Config { private filteredKeys?: string[]; private notifiedFilteredKeys = new Set(); + /** + * Instantiates the config reader from a list of application config objects. + */ static fromConfigs(configs: AppConfig[]): ConfigReader { if (configs.length === 0) { return new ConfigReader(undefined); @@ -96,6 +99,7 @@ export class ConfigReader implements Config { private readonly prefix: string = '', ) {} + /** {@inheritdoc Config.has} */ has(key: string): boolean { const value = this.readValue(key); if (value !== undefined) { @@ -104,12 +108,14 @@ export class ConfigReader implements Config { return this.fallback?.has(key) ?? false; } + /** {@inheritdoc Config.keys} */ keys(): string[] { const localKeys = this.data ? Object.keys(this.data) : []; const fallbackKeys = this.fallback?.keys() ?? []; return [...new Set([...localKeys, ...fallbackKeys])]; } + /** {@inheritdoc Config.get} */ get(key?: string): T { const value = this.getOptional(key); if (value === undefined) { @@ -118,6 +124,7 @@ export class ConfigReader implements Config { return value as T; } + /** {@inheritdoc Config.getOptional} */ getOptional(key?: string): T | undefined { const value = this.readValue(key); const fallbackValue = this.fallback?.getOptional(key); @@ -154,6 +161,7 @@ export class ConfigReader implements Config { ).value as T; } + /** {@inheritdoc Config.getConfig} */ getConfig(key: string): ConfigReader { const value = this.getOptionalConfig(key); if (value === undefined) { @@ -162,6 +170,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalConfig} */ getOptionalConfig(key: string): ConfigReader | undefined { const value = this.readValue(key); const fallbackConfig = this.fallback?.getOptionalConfig(key); @@ -177,6 +186,7 @@ export class ConfigReader implements Config { return fallbackConfig; } + /** {@inheritdoc Config.getConfigArray} */ getConfigArray(key: string): ConfigReader[] { const value = this.getOptionalConfigArray(key); if (value === undefined) { @@ -185,6 +195,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalConfigArray} */ getOptionalConfigArray(key: string): ConfigReader[] | undefined { const configs = this.readConfigValue(key, values => { if (!Array.isArray(values)) { @@ -220,6 +231,7 @@ export class ConfigReader implements Config { return configs.map((obj, index) => this.copy(obj, `${key}[${index}]`)); } + /** {@inheritdoc Config.getNumber} */ getNumber(key: string): number { const value = this.getOptionalNumber(key); if (value === undefined) { @@ -228,6 +240,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalNumber} */ getOptionalNumber(key: string): number | undefined { const value = this.readConfigValue( key, @@ -247,6 +260,7 @@ export class ConfigReader implements Config { return number; } + /** {@inheritdoc Config.getBoolean} */ getBoolean(key: string): boolean { const value = this.getOptionalBoolean(key); if (value === undefined) { @@ -255,6 +269,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalBoolean} */ getOptionalBoolean(key: string): boolean | undefined { return this.readConfigValue( key, @@ -262,6 +277,7 @@ export class ConfigReader implements Config { ); } + /** {@inheritdoc Config.getString} */ getString(key: string): string { const value = this.getOptionalString(key); if (value === undefined) { @@ -270,6 +286,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalString} */ getOptionalString(key: string): string | undefined { return this.readConfigValue( key, @@ -278,6 +295,7 @@ export class ConfigReader implements Config { ); } + /** {@inheritdoc Config.getStringArray} */ getStringArray(key: string): string[] { const value = this.getOptionalStringArray(key); if (value === undefined) { @@ -286,6 +304,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalStringArray} */ getOptionalStringArray(key: string): string[] | undefined { return this.readConfigValue(key, values => { if (!Array.isArray(values)) { From bd7f1e506ca30468b64a5943d0c2bd1fa2b8a370 Mon Sep 17 00:00:00 2001 From: Victor Perera Date: Tue, 26 Oct 2021 16:43:45 -0500 Subject: [PATCH 157/245] Tests fixed Signed-off-by: Victor Perera --- .../src/components/EntityTypePicker/EntityTypePicker.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 232f4f250d..8eba11aeca 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -129,7 +129,6 @@ describe('', () => { Date: Thu, 21 Oct 2021 12:28:43 -0700 Subject: [PATCH 158/245] fixed prettier issues with FileExplorer files Signed-off-by: Jeremy Guarini --- .../FileExplorer/FileExplorer.test.tsx | 33 ++++++++++++++----- .../components/FileExplorer/FileExplorer.tsx | 13 ++++---- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx index 2162c25139..ff5845edfe 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx @@ -1,3 +1,18 @@ +/* + * 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 { groupByPath } from './FileExplorer'; const dummyFiles = [ @@ -23,8 +38,8 @@ const dummyFiles = [ coverage: 1, missing: 1, tracked: 1, - path: '' - } + path: '', + }, ]; const dummyDataGroupedByPath = { @@ -35,7 +50,7 @@ const dummyDataGroupedByPath = { coverage: 1, missing: 1, tracked: 1, - path: '' + path: '', }, { filename: 'dir1/file2', @@ -43,8 +58,8 @@ const dummyDataGroupedByPath = { coverage: 1, missing: 1, tracked: 1, - path: '' - } + path: '', + }, ], dir2: [ { @@ -53,13 +68,13 @@ const dummyDataGroupedByPath = { coverage: 1, missing: 1, tracked: 1, - path: '' - } - ] + path: '', + }, + ], }; describe('groupByPath function', () => { it('should group files by their root directory,as per their filename', () => { expect(groupByPath(dummyFiles)).toBe(dummyDataGroupedByPath); }); -}); \ No newline at end of file +}); diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx index bc36af265d..fbe74ff7e5 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -53,12 +53,10 @@ type CoverageTableRow = { export const groupByPath = (files: CoverageTableRow[]) => { const acc: FileStructureObject = {}; - files.forEach (file => { + files.forEach(file => { const filename = file.filename; if (!file.filename) return; - const pathArray = filename?.split('/').filter( - el => el !== '' - ); + const pathArray = filename?.split('/').filter(el => el !== ''); if (pathArray) { if (!acc.hasOwnProperty(pathArray[0])) { acc[pathArray[0]] = []; @@ -69,11 +67,14 @@ export const groupByPath = (files: CoverageTableRow[]) => { return acc; }; -const removeVisitedPathGroup = (files: CoverageTableRow[], pathGroup: string) => { +const removeVisitedPathGroup = ( + files: CoverageTableRow[], + pathGroup: string, +) => { return files.map(file => { return { ...file, - filename: file.filename + filename: file.filename ? file.filename.substring( file.filename?.indexOf(pathGroup) + pathGroup.length + 1, ) From b3fb448e7bd22db2da17be0eae1feab2c8d4026a Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Mon, 25 Oct 2021 08:53:56 -0700 Subject: [PATCH 159/245] removed extra line in imports Signed-off-by: Jeremy Guarini --- .../code-coverage/src/components/FileExplorer/FileExplorer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx index fbe74ff7e5..ed31cb9c62 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -30,7 +30,6 @@ import { useAsync } from 'react-use'; import { codeCoverageApiRef } from '../../api'; import { FileEntry } from '../../types'; import { FileContent } from './FileContent'; - import { Progress, ResponseErrorPanel, From 7ea444c78d28af004a9c11bf2f8db0af9234c028 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Tue, 26 Oct 2021 10:26:23 -0700 Subject: [PATCH 160/245] update filexplorer test Signed-off-by: Jeremy Guarini --- .../src/components/FileExplorer/FileExplorer.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx index ff5845edfe..948d9c9722 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx @@ -75,6 +75,6 @@ const dummyDataGroupedByPath = { describe('groupByPath function', () => { it('should group files by their root directory,as per their filename', () => { - expect(groupByPath(dummyFiles)).toBe(dummyDataGroupedByPath); + expect(groupByPath(dummyFiles)).toStrictEqual(dummyDataGroupedByPath); }); }); From 6f8afb401b6962832426e770c5f087f28a6be3ff Mon Sep 17 00:00:00 2001 From: Tony Tam Date: Mon, 25 Oct 2021 08:15:43 -0700 Subject: [PATCH 161/245] added Splunk Signed-off-by: Tony Tam --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 451dc7a953..0367a182bd 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -60,3 +60,4 @@ | [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | | [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | | [Adevinta](https://www.adevinta.com) | [Ray Sinnemma](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | +| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation.| From 80afaa8929eeff939784712d869e82896e8e2f94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Oct 2021 04:10:16 +0000 Subject: [PATCH 162/245] build(deps): bump @microsoft/microsoft-graph-types from 2.6.0 to 2.8.0 Bumps [@microsoft/microsoft-graph-types](https://github.com/microsoftgraph/msgraph-typescript-typings) from 2.6.0 to 2.8.0. - [Release notes](https://github.com/microsoftgraph/msgraph-typescript-typings/releases) - [Commits](https://github.com/microsoftgraph/msgraph-typescript-typings/compare/2.6.0...2.8.0) --- updated-dependencies: - dependency-name: "@microsoft/microsoft-graph-types" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 80b35d32ae..bbe2324f73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4574,9 +4574,9 @@ integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== "@microsoft/microsoft-graph-types@^2.6.0": - version "2.6.0" - resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.6.0.tgz#d1f97049362ea5c0ed79199875f24707f9832b29" - integrity sha512-wK26kqUdQJLpgPh2Fk5jcRuFtnGLjNrXCWKM78zXnvAg+On246WYLz6+7amILJDC5rpaZTGXf4eynXoJFuNBTw== + version "2.8.0" + resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.8.0.tgz#c3b538f99028e8609c5ebf95a494318a8f3d9201" + integrity sha512-NDgLn9IhYD/+nCeeGAi1JM7xTFqaM6rkXfLfiC1xvXy48BGBUrAf8fNFq5fkzBvGY8HfjzdPIkrJkfvLL+rzDQ== "@microsoft/tsdoc-config@~0.15.2": version "0.15.2" From 9dcad0bdad6d8325741290c40695f66a96e740a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Zynger?= Date: Wed, 27 Oct 2021 08:32:24 +0200 Subject: [PATCH 163/245] Add SoundCloud Signed-off-by: Julio Zynger --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 0367a182bd..d3c4e8c969 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -61,3 +61,4 @@ | [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | | [Adevinta](https://www.adevinta.com) | [Ray Sinnemma](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | | [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation.| +| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc.| From 40cfec8b3fecb85f8403afd1b0ba52b1336c60d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 09:22:42 +0200 Subject: [PATCH 164/245] More theme API cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/forty-cooks-film.md | 5 +++ packages/theme/api-report.md | 66 +++++++++------------------------ packages/theme/src/baseTheme.ts | 19 ++++++++-- packages/theme/src/pageTheme.ts | 48 +++++++++++++++++------- packages/theme/src/themes.ts | 10 +++++ packages/theme/src/types.ts | 56 ++++++++++++++++++++++++---- 6 files changed, 131 insertions(+), 73 deletions(-) create mode 100644 .changeset/forty-cooks-film.md diff --git a/.changeset/forty-cooks-film.md b/.changeset/forty-cooks-film.md new file mode 100644 index 0000000000..20ff746949 --- /dev/null +++ b/.changeset/forty-cooks-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +More theme API cleanup diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 41ea2420f2..72d7887214 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -9,9 +9,7 @@ import { PaletteOptions } from '@material-ui/core/styles/createPalette'; import { Theme } from '@material-ui/core'; import { ThemeOptions } from '@material-ui/core'; -// Warning: (ae-missing-release-tag) "BackstagePalette" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BackstagePalette = Palette & BackstagePaletteAdditions; // @public @@ -69,101 +67,71 @@ export type BackstagePaletteAdditions = { }; }; -// Warning: (ae-missing-release-tag) "BackstagePaletteOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BackstagePaletteOptions = PaletteOptions & BackstagePaletteAdditions; -// Warning: (ae-missing-release-tag) "BackstageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface BackstageTheme extends Theme { // (undocumented) - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; // (undocumented) page: PageTheme; // (undocumented) palette: BackstagePalette; } -// Warning: (ae-missing-release-tag) "BackstageThemeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface BackstageThemeOptions extends ThemeOptions { // (undocumented) - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; // (undocumented) page: PageTheme; // (undocumented) palette: BackstagePaletteOptions; } -// Warning: (ae-missing-release-tag) "colorVariants" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const colorVariants: Record; -// Warning: (ae-missing-release-tag) "createTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createTheme(options: SimpleThemeOptions): BackstageTheme; -// Warning: (ae-missing-release-tag) "createThemeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createThemeOptions( options: SimpleThemeOptions, ): BackstageThemeOptions; -// Warning: (ae-missing-release-tag) "createThemeOverrides" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createThemeOverrides(theme: BackstageTheme): Overrides; -// Warning: (ae-missing-release-tag) "darkTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const darkTheme: BackstageTheme; -// Warning: (ae-missing-release-tag) "genPageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function genPageTheme(colors: string[], shape: string): PageTheme; -// Warning: (ae-missing-release-tag) "lightTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const lightTheme: BackstageTheme; -// Warning: (ae-missing-release-tag) "PageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PageTheme = { colors: string[]; shape: string; backgroundImage: string; }; -// Warning: (ae-missing-release-tag) "pageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const pageTheme: Record; -// Warning: (ae-missing-release-tag) "PageThemeSelector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PageThemeSelector = { themeId: string; }; -// Warning: (ae-missing-release-tag) "shapes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const shapes: Record; -// Warning: (ae-missing-release-tag) "SimpleThemeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type SimpleThemeOptions = { palette: BackstagePaletteOptions; diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 4a45516a55..da663e3398 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -17,7 +17,6 @@ import { createTheme as createMuiTheme } from '@material-ui/core/styles'; import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; import { Overrides } from '@material-ui/core/styles/overrides'; - import { BackstageTheme, BackstageThemeOptions, @@ -28,6 +27,11 @@ import { pageTheme as defaultPageThemes } from './pageTheme'; const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; +/** + * A helper for creating theme options. + * + * @public + */ export function createThemeOptions( options: SimpleThemeOptions, ): BackstageThemeOptions { @@ -84,6 +88,11 @@ export function createThemeOptions( }; } +/** + * A helper for creating theme overrides. + * + * @public + */ export function createThemeOverrides(theme: BackstageTheme): Overrides { return { MuiCssBaseline: { @@ -271,8 +280,12 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { }; } -// Creates a Backstage MUI theme using a palette. -// The theme is created with the common Backstage options and component styles. +/** + * Creates a Backstage MUI theme using a palette. The theme is created with the + * common Backstage options and component styles. + * + * @public + */ export function createTheme(options: SimpleThemeOptions): BackstageTheme { const themeOptions = createThemeOptions(options); const baseTheme = createMuiTheme(themeOptions) as BackstageTheme; diff --git a/packages/theme/src/pageTheme.ts b/packages/theme/src/pageTheme.ts index 5765e13c23..ae81dc8534 100644 --- a/packages/theme/src/pageTheme.ts +++ b/packages/theme/src/pageTheme.ts @@ -15,22 +15,33 @@ */ import { PageTheme } from './types'; -/* - # How to add a shape - 1. Get the svg shape from figma, should be ~1400 wide, ~400 high - and only the white->transparent mask, no colors. - 2. Run it through https://jakearchibald.github.io/svgomg/ - 3. Run that through https://github.com/tigt/mini-svg-data-uri - with something like https://npm.runkit.com/mini-svg-data-uri - 4. Wrap the output in `url("")` - 5. Give it a name and paste it into the `shapes` object below. -*/ +/** + * The default predefined burst shapes. + * + * @public + * @remarks + * + * How to add a shape: + * + * 1. Get the svg shape from figma, should be ~1400 wide, ~400 high + * and only the white-to-transparent mask, no colors. + * 2. Run it through https://jakearchibald.github.io/svgomg/ + * 3. Run that through https://github.com/tigt/mini-svg-data-uri + * with something like https://npm.runkit.com/mini-svg-data-uri + * 4. Wrap the output in `url("")` + * 5. Give it a name and paste it into the `shapes` object below. + */ export const shapes: Record = { wave: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1368' height='401' x='0' y='0' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M437 116C223 116 112 0 112 0h1256v400c-82 0-225-21-282-109-112-175-436-175-649-175z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1368 400V282C891-29 788 40 711 161 608 324 121 372 0 361v39h1368z'/%3e%3cpath fill='url(%23paint2_linear)' d='M1368 244v156H0V94c92-24 198-46 375 0l135 41c176 51 195 109 858 109z'/%3e%3cpath fill='url(%23paint3_linear)' d='M1252 400h116c-14-7-35-14-116-16-663-14-837-128-1013-258l-85-61C98 28 46 8 0 0v400h1252z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M-172-98h1671v601H-172z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='602' x2='1093.5' y1='-960.5' y2='272' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='482' x2='480' y1='1058.5' y2='70.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='424' x2='446.1' y1='-587.5' y2='274.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='587' x2='349' y1='-1120.5' y2='341' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, wave2: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1764' height='479' x='-229' y='-6' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M0 400h1350C1321 336 525 33 179-2c-345-34-395 236-408 402H0z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1378 177v223H0V217s219 75 327 52C436 246 717-35 965 45s254 144 413 132z'/%3e%3cpath fill='url(%23paint2_linear)' d='M26 400l-78-16c-170 205-44-6-137-30l-4-1 4 1 137 30c37-45 89-110 159-201 399-514-45 238 1176-50 275-65 354-39 91 267H26z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='431' x2='397.3' y1='-599' y2='372.8' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='236.5' x2='446.6' y1='-586' y2='381.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='851.8' x2='640.4' y1='-867.2' y2='363.7' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, round: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='2269' height='1408' x='-610' y='-509' maskUnits='userSpaceOnUse'%3e%3ccircle cx='1212.8' cy='74.8' r='317.5' fill='url(%23paint0_linear)' transform='rotate(-52 1213 75)'/%3e%3ccircle cx='737.8' cy='445.8' r='317.5' fill='url(%23paint1_linear)' transform='rotate(-116 738 446)'/%3e%3ccircle cx='601.8' cy='52.8' r='418.6' fill='url(%23paint2_linear)' transform='rotate(-117 602 53)'/%3e%3ccircle cx='999.8' cy='364' r='389.1' fill='url(%23paint3_linear)' transform='rotate(31 1000 364)'/%3e%3cellipse cx='-109.2' cy='263.5' fill='url(%23paint4_linear)' rx='429.2' ry='465.8' transform='rotate(-85 -109 264)'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='1301.2' x2='161.4' y1='-1879.7' y2='-969.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='826.2' x2='-313.6' y1='-1508.7' y2='-598.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='718.4' x2='-784.3' y1='-2524' y2='-1324.2' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='1108.2' x2='-288.6' y1='-2031.1' y2='-915.9' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint4_linear' x1='10.4' x2='-1626.5' y1='-2603.8' y2='-1399.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, }; +/** + * The color range variants that are used in e.g. colorful bursts. + * + * @public + */ export const colorVariants: Record = { darkGrey: ['#171717', '#383838'], marineBlue: ['#006D8F', '#0049A1'], @@ -43,9 +54,15 @@ export const colorVariants: Record = { pinkSea: ['#C8077A', '#C2297D'], }; -// As the background shapes and colors are decorative, we place them onto -// the page as a css background-image instead of an html element of its own. -// Utility to not have to write colors and shapes twice. +/** + * Utility to not have to write colors and shapes twice. + * + * @public + * @remarks + * + * As the background shapes and colors are decorative, we place them onto the + * page as a css background-image instead of an html element of its own. + */ export function genPageTheme(colors: string[], shape: string): PageTheme { const gradientColors = colors.length === 1 ? [colors[0], colors[0]] : colors; const gradient = `linear-gradient(90deg, ${gradientColors.join(', ')})`; @@ -54,6 +71,11 @@ export function genPageTheme(colors: string[], shape: string): PageTheme { return { colors, shape, backgroundImage }; } +/** + * All of the builtin page themes. + * + * @public + */ export const pageTheme: Record = { home: genPageTheme(colorVariants.teal, shapes.wave), documentation: genPageTheme(colorVariants.pinkSea, shapes.wave2), diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index d7a4a6d785..3eb1bcc252 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -18,6 +18,11 @@ import { createTheme } from './baseTheme'; import { pageTheme } from './pageTheme'; import { yellow } from '@material-ui/core/colors'; +/** + * The default Backstage light theme. + * + * @public + */ export const lightTheme = createTheme({ palette: { type: 'light', @@ -83,6 +88,11 @@ export const lightTheme = createTheme({ pageTheme, }); +/** + * The default Backstage dark theme. + * + * @public + */ export const darkTheme = createTheme({ palette: { type: 'dark', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 770b55d341..db3e89fa4b 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -79,28 +79,63 @@ export type BackstagePaletteAdditions = { }; }; +/** + * The full Backstage palette. + * + * @public + */ export type BackstagePalette = Palette & BackstagePaletteAdditions; + +/** + * The full Backstage palette options. + * + * @public + */ export type BackstagePaletteOptions = PaletteOptions & BackstagePaletteAdditions; +/** + * Selector for what page theme to use. + * + * @public + */ export type PageThemeSelector = { themeId: string; }; +/** + * A Backstage theme. + * + * @public + */ export interface BackstageTheme extends Theme { palette: BackstagePalette; page: PageTheme; - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; -} - -export interface BackstageThemeOptions extends ThemeOptions { - palette: BackstagePaletteOptions; - page: PageTheme; - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; } /** - * A simpler configuration for creating a new theme that just tweaks some parts of the backstage one. + * Backstage theme options. + * + * @public + * @remarks + * + * This is essentially a partial theme definition made by the user, that then + * gets merged together with defaults and other values to form the final + * {@link BackstageTheme}. + * + */ +export interface BackstageThemeOptions extends ThemeOptions { + palette: BackstagePaletteOptions; + page: PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; +} + +/** + * A simpler configuration for creating a new theme that just tweaks some parts + * of the backstage one. + * + * @public */ export type SimpleThemeOptions = { palette: BackstagePaletteOptions; @@ -109,6 +144,11 @@ export type SimpleThemeOptions = { fontFamily?: string; }; +/** + * The theme definitions for a given layout page. + * + * @public + */ export type PageTheme = { colors: string[]; shape: string; From 50eba2374b4d4ce2033714a94b0fe6b056adde9f Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 09:16:23 +0100 Subject: [PATCH 165/245] revert prettier change to scaffolder examples Signed-off-by: Brian Fletcher --- .../bitbucket-demo/template/catalog-info.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml index 3b685a57e0..875664d2a8 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: { { cookiecutter.name | jsonify } } + name: {{cookiecutter.name | jsonify}} spec: type: website lifecycle: experimental - owner: { { cookiecutter.owner | jsonify } } + owner: {{cookiecutter.owner | jsonify}} From 5a98c405f52b070918afb44f7116ecbaa6a6432a Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 10:48:17 +0100 Subject: [PATCH 166/245] rename task agent to task manager Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 80 +++++++++---------- .../tasks/DefaultWorkflowRunner.test.ts | 4 +- .../tasks/HandlebarsWorkflowRunner.ts | 4 +- .../tasks/LegacyWorkflowRunner.test.ts | 4 +- .../tasks/NunjucksWorkflowRunner.ts | 12 ++- .../tasks/StorageTaskBroker.test.ts | 12 +-- .../src/scaffolder/tasks/StorageTaskBroker.ts | 12 +-- .../src/scaffolder/tasks/TaskWorker.ts | 4 +- .../src/scaffolder/tasks/index.ts | 4 +- .../src/scaffolder/tasks/types.ts | 6 +- 10 files changed, 74 insertions(+), 68 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index d0f4cb36e8..5441c4de6b 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -349,48 +349,10 @@ export type Status = | 'cancelled' | 'completed'; -// @public -export interface Task { - // (undocumented) - complete(result: CompletedTaskState, metadata?: JsonValue): Promise; - // (undocumented) - done: boolean; - // (undocumented) - emitLog(message: string, metadata?: JsonValue): Promise; - // (undocumented) - getWorkspaceName(): Promise; - // (undocumented) - secrets?: TaskSecrets; - // (undocumented) - spec: TaskSpec; -} - -// @public -export class TaskAgent implements Task { - // (undocumented) - complete(result: CompletedTaskState, metadata?: JsonObject): Promise; - // (undocumented) - static create( - state: TaskState, - storage: TaskStore, - logger: Logger_2, - ): TaskAgent; - // (undocumented) - get done(): boolean; - // (undocumented) - emitLog(message: string, metadata?: JsonObject): Promise; - // (undocumented) - getWorkspaceName(): Promise; - // (undocumented) - get secrets(): TaskSecrets | undefined; - // (undocumented) - get spec(): TaskSpec; -} - // @public export interface TaskBroker { // (undocumented) - claim(): Promise; + claim(): Promise; // (undocumented) dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; // (undocumented) @@ -412,9 +374,47 @@ export interface TaskBroker { vacuumTasks(timeoutS: { timeoutS: number }): Promise; } +// @public +export interface TaskContext { + // (undocumented) + complete(result: CompletedTaskState, metadata?: JsonValue): Promise; + // (undocumented) + done: boolean; + // (undocumented) + emitLog(message: string, metadata?: JsonValue): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; +} + // @public export type TaskEventType = 'completion' | 'log'; +// @public +export class TaskManager implements TaskContext { + // (undocumented) + complete(result: CompletedTaskState, metadata?: JsonObject): Promise; + // (undocumented) + static create( + state: TaskState, + storage: TaskStore, + logger: Logger_2, + ): TaskManager; + // (undocumented) + get done(): boolean; + // (undocumented) + emitLog(message: string, metadata?: JsonObject): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + get secrets(): TaskSecrets | undefined; + // (undocumented) + get spec(): TaskSpec; +} + // @public export type TaskSecrets = { token: string | undefined; @@ -525,7 +525,7 @@ export class TaskWorker { // (undocumented) static createWorker(options: CreateWorkerOptions): TaskWorker; // (undocumented) - runOneTask(task: Task): Promise; + runOneTask(task: TaskContext): Promise; // (undocumented) start(): void; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts index 0a843f4ddf..d07ff29fae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts @@ -22,7 +22,7 @@ import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { Task, TaskSpec } from './types'; +import { TaskContext, TaskSpec } from './types'; describe('DefaultWorkflowRunner', () => { const logger = getVoidLogger(); @@ -38,7 +38,7 @@ describe('DefaultWorkflowRunner', () => { }), ); - const createMockTaskWithSpec = (spec: TaskSpec): Task => ({ + const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ spec, complete: async () => {}, done: false, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 831ef98784..77d0a7be09 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { - Task, + TaskContext, WorkflowRunner, WorkflowResponse, TaskSpecV1beta2, @@ -72,7 +72,7 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { this.handlebars.registerHelper('eq', (a, b) => a === b); } - async execute(task: Task): Promise { + async execute(task: TaskContext): Promise { if (!isValidTaskSpec(task.spec)) { throw new InputError(`Task spec is not a valid v1beta2 task spec`); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts index aa46747c68..91e772dc4e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts @@ -21,7 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; -import { Task, TaskSpec } from './types'; +import { TaskContext, TaskSpec } from './types'; import { RepoSpec } from '../actions/builtin/publish/util'; describe('LegacyWorkflowRunner', () => { @@ -37,7 +37,7 @@ describe('LegacyWorkflowRunner', () => { }), ); - const createMockTaskWithSpec = (spec: TaskSpec): Task => ({ + const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ spec, complete: async () => {}, done: false, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 5cceb16324..587a93e1f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -15,7 +15,7 @@ */ import { ScmIntegrations } from '@backstage/integration'; import { - Task, + TaskContext, TaskSpec, TaskSpecV1beta3, TaskStep, @@ -52,7 +52,13 @@ const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { return taskSpec.apiVersion === 'scaffolder.backstage.io/v1beta3'; }; -const createStepLogger = ({ task, step }: { task: Task; step: TaskStep }) => { +const createStepLogger = ({ + task, + step, +}: { + task: TaskContext; + step: TaskStep; +}) => { const metadata = { stepId: step.id }; const taskLogger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', @@ -162,7 +168,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { }); } - async execute(task: Task): Promise { + async execute(task: TaskContext): Promise { if (!isValidTaskSpec(task.spec)) { throw new InputError( 'Wrong template version executed with the workflow engine', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index c5d94c1324..76fb8e00f0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; -import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; +import { StorageTaskBroker, TaskManager } from './StorageTaskBroker'; import { TaskSecrets, TaskSpec, SerializedTaskEvent } from './types'; async function createStore(): Promise { @@ -48,7 +48,7 @@ describe('StorageTaskBroker', () => { it('should claim a dispatched work item', async () => { const broker = new StorageTaskBroker(storage, logger); await broker.dispatch({} as TaskSpec); - await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); + await expect(broker.claim()).resolves.toEqual(expect.any(TaskManager)); }); it('should wait for a dispatched work item', async () => { @@ -58,7 +58,7 @@ describe('StorageTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); await broker.dispatch({} as TaskSpec); - await expect(promise).resolves.toEqual(expect.any(TaskAgent)); + await expect(promise).resolves.toEqual(expect.any(TaskManager)); }); it('should dispatch multiple items and claim them in order', async () => { @@ -70,9 +70,9 @@ describe('StorageTaskBroker', () => { const taskA = await broker.claim(); const taskB = await broker.claim(); const taskC = await broker.claim(); - await expect(taskA).toEqual(expect.any(TaskAgent)); - await expect(taskB).toEqual(expect.any(TaskAgent)); - await expect(taskC).toEqual(expect.any(TaskAgent)); + await expect(taskA).toEqual(expect.any(TaskManager)); + await expect(taskB).toEqual(expect.any(TaskManager)); + await expect(taskC).toEqual(expect.any(TaskManager)); await expect(taskA.spec.steps[0].id).toBe('a'); await expect(taskB.spec.steps[0].id).toBe('b'); await expect(taskC.spec.steps[0].id).toBe('c'); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 2f4387ca4a..7845ca994a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -18,7 +18,7 @@ import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; import { CompletedTaskState, - Task, + TaskContext, TaskSecrets, TaskSpec, TaskStore, @@ -29,17 +29,17 @@ import { } from './types'; /** - * TaskAgent + * TaskManager * * @public */ -export class TaskAgent implements Task { +export class TaskManager implements TaskContext { private isDone = false; private heartbeatTimeoutId?: ReturnType; static create(state: TaskState, storage: TaskStore, logger: Logger) { - const agent = new TaskAgent(state, storage, logger); + const agent = new TaskManager(state, storage, logger); agent.startTimeout(); return agent; } @@ -135,11 +135,11 @@ export class StorageTaskBroker implements TaskBroker { ) {} private deferredDispatch = defer(); - async claim(): Promise { + async claim(): Promise { for (;;) { const pendingTask = await this.storage.claimTask(); if (pendingTask) { - return TaskAgent.create( + return TaskManager.create( { taskId: pendingTask.id, spec: pendingTask.spec, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index fcc215be99..d22bdc4e30 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Task, TaskBroker, WorkflowRunner } from './types'; +import { TaskContext, TaskBroker, WorkflowRunner } from './types'; import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { Logger } from 'winston'; @@ -94,7 +94,7 @@ export class TaskWorker { })(); } - async runOneTask(task: Task) { + async runOneTask(task: TaskContext) { try { const { output } = task.spec.apiVersion === 'scaffolder.backstage.io/v1beta3' diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 941c27ef0e..510c862dae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ export { DatabaseTaskStore } from './DatabaseTaskStore'; -export { TaskAgent } from './StorageTaskBroker'; +export { TaskManager } from './StorageTaskBroker'; export type { TaskState } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; export type { CreateWorkerOptions } from './TaskWorker'; @@ -31,7 +31,7 @@ export type { Status, TaskEventType, TaskBroker, - Task, + TaskContext, TaskStore, DispatchResult, } from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index f2e13923ec..6b3075844d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -139,7 +139,7 @@ export type DispatchResult = { * * @public */ -export interface Task { +export interface TaskContext { spec: TaskSpec; secrets?: TaskSecrets; done: boolean; @@ -154,7 +154,7 @@ export interface Task { * @public */ export interface TaskBroker { - claim(): Promise; + claim(): Promise; dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; vacuumTasks(timeoutS: { timeoutS: number }): Promise; observe( @@ -221,5 +221,5 @@ export interface TaskStore { export type WorkflowResponse = { output: { [key: string]: JsonValue } }; export interface WorkflowRunner { - execute(task: Task): Promise; + execute(task: TaskContext): Promise; } From 36e2b548cbf5e9a793d47f1d3212c9f45e14b4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 11:51:43 +0200 Subject: [PATCH 167/245] Fixing up integration-react as well MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/olive-clouds-change.md | 5 +++ packages/integration-react/api-report.md | 31 ++++++++----------- packages/integration-react/src/api/ScmAuth.ts | 3 ++ .../integration-react/src/api/ScmAuthApi.ts | 12 +++++-- .../src/api/ScmIntegrationsApi.ts | 15 +++++++++ .../ScmIntegrationIcon/ScmIntegrationIcon.tsx | 20 +++++++++++- .../components/ScmIntegrationIcon/index.ts | 1 + 7 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 .changeset/olive-clouds-change.md diff --git a/.changeset/olive-clouds-change.md b/.changeset/olive-clouds-change.md new file mode 100644 index 0000000000..7b367710ac --- /dev/null +++ b/.changeset/olive-clouds-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-react': patch +--- + +Clean up the API exports diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index b7feb6b6a9..2db29631a3 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -65,7 +65,6 @@ export class ScmAuth implements ScmAuthApi { host?: string; }, ): ScmAuth; - // (undocumented) getCredentials(options: ScmAuthTokenOptions): Promise; isUrlSupported(url: URL): boolean; static merge(...providers: ScmAuth[]): ScmAuthApi; @@ -79,7 +78,7 @@ export interface ScmAuthApi { // @public export const scmAuthApiRef: ApiRef; -// @public (undocumented) +// @public export interface ScmAuthTokenOptions extends AuthRequestOptions { additionalScope?: { repoWrite?: boolean; @@ -87,7 +86,7 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions { url: string; } -// @public (undocumented) +// @public export interface ScmAuthTokenResponse { headers: { [name: string]: string; @@ -95,25 +94,21 @@ export interface ScmAuthTokenResponse { token: string; } -// Warning: (ae-missing-release-tag) "ScmIntegrationIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const ScmIntegrationIcon: ({ - type, -}: { - type?: string | undefined; -}) => JSX.Element; +// @public +export const ScmIntegrationIcon: ( + props: ScmIntegrationIconProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "ScmIntegrationsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type ScmIntegrationIconProps = { + type?: string; +}; + +// @public export class ScmIntegrationsApi { - // (undocumented) static fromConfig(config: Config): ScmIntegrationRegistry; } -// Warning: (ae-missing-release-tag) "scmIntegrationsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const scmIntegrationsApiRef: ApiRef; ``` diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index c34c4da758..be3216962d 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -254,6 +254,9 @@ export class ScmAuth implements ScmAuthApi { return url.host === this.#host; } + /** + * Fetches credentials for the given resource. + */ async getCredentials( options: ScmAuthTokenOptions, ): Promise { diff --git a/packages/integration-react/src/api/ScmAuthApi.ts b/packages/integration-react/src/api/ScmAuthApi.ts index d8fc413620..44b007c4a2 100644 --- a/packages/integration-react/src/api/ScmAuthApi.ts +++ b/packages/integration-react/src/api/ScmAuthApi.ts @@ -20,7 +20,11 @@ import { AuthRequestOptions, } from '@backstage/core-plugin-api'; -/** @public */ +/** + * The options that control a {@link ScmAuthApi.getCredentials} call. + * + * @public + */ export interface ScmAuthTokenOptions extends AuthRequestOptions { /** * The URL of the SCM resource to be accessed. @@ -43,7 +47,11 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions { }; } -/** @public */ +/** + * The response from a {@link ScmAuthApi.getCredentials} call. + * + * @public + */ export interface ScmAuthTokenResponse { /** * An authorization token that can be used to authenticate requests. diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.ts b/packages/integration-react/src/api/ScmIntegrationsApi.ts index 6bf2ba8a6d..06f28a6c4d 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.ts @@ -21,12 +21,27 @@ import { } from '@backstage/integration'; import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; +/** + * Factory class for creating {@link @backstage/integration#ScmIntegrationRegistry} instances. + * + * @public + */ export class ScmIntegrationsApi { + /** + * Instantiates an {@link @backstage/integration#ScmIntegrationRegistry}. + * + * @param config - The root of the config hierarchy. + */ static fromConfig(config: Config): ScmIntegrationRegistry { return ScmIntegrations.fromConfig(config); } } +/** + * The API that holds all configured SCM integrations. + * + * @public + */ export const scmIntegrationsApiRef: ApiRef = createApiRef({ id: 'integration.scmintegrations', diff --git a/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx b/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx index 80567d5cc3..dd6ba18690 100644 --- a/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx +++ b/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx @@ -17,7 +17,25 @@ import CodeIcon from '@material-ui/icons/Code'; import React from 'react'; import { useApp } from '@backstage/core-plugin-api'; -export const ScmIntegrationIcon = ({ type }: { type?: string }) => { +/** + * Props for {@link ScmIntegrationIcon}. + * + * @public + */ +export type ScmIntegrationIconProps = { + /** + * The integration type, e.g. "github". + */ + type?: string; +}; + +/** + * An icon that represents a certain SCM integration. + * + * @public + */ +export const ScmIntegrationIcon = (props: ScmIntegrationIconProps) => { + const { type } = props; const app = useApp(); const DefaultIcon = CodeIcon; const Icon = type ? app.getSystemIcon(type) ?? DefaultIcon : DefaultIcon; diff --git a/packages/integration-react/src/components/ScmIntegrationIcon/index.ts b/packages/integration-react/src/components/ScmIntegrationIcon/index.ts index 86e60e5b99..a487484d39 100644 --- a/packages/integration-react/src/components/ScmIntegrationIcon/index.ts +++ b/packages/integration-react/src/components/ScmIntegrationIcon/index.ts @@ -15,3 +15,4 @@ */ export { ScmIntegrationIcon } from './ScmIntegrationIcon'; +export type { ScmIntegrationIconProps } from './ScmIntegrationIcon'; From 2223b6de60bccd7e85048727762bfcdba08590d8 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 10:54:32 +0100 Subject: [PATCH 168/245] adds a todo message to the router import Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/src/service/router.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 71b6d27b5e..1c5343d458 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -40,6 +40,12 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +/** + * TODO: The following should import directly from the router file. + * Due to a circular dependency between this plugin and the + * plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error: + * TypeError: _pluginscaffolderbackend.createTemplateAction is not a function + */ import { createRouter } from '../index'; const createCatalogClient = (templates: any[] = []) => From 3236071136feb402a2b6fa107677e90d4176860c Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 12:46:47 +0100 Subject: [PATCH 169/245] change name of task worker builder function also makes it async Signed-off-by: Brian Fletcher --- .../src/scaffolder/tasks/TaskWorker.test.ts | 6 +++--- .../scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts | 2 +- plugins/scaffolder-backend/src/service/router.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 3cb73abb85..f33b9182ce 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -82,7 +82,7 @@ describe('TaskWorker', () => { it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = TaskWorker.createWorker({ + const taskWorker = TaskWorker.create({ logger, workingDirectory, integrations, @@ -113,7 +113,7 @@ describe('TaskWorker', () => { it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = TaskWorker.createWorker({ + const taskWorker = TaskWorker.create({ logger, workingDirectory, integrations, @@ -142,7 +142,7 @@ describe('TaskWorker', () => { }); const broker = new StorageTaskBroker(storage, logger); - const taskWorker = TaskWorker.createWorker({ + const taskWorker = TaskWorker.create({ logger, workingDirectory, integrations, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index d22bdc4e30..8d89735361 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -56,7 +56,7 @@ export type CreateWorkerOptions = { export class TaskWorker { private constructor(private readonly options: TaskWorkerOptions) {} - static createWorker(options: CreateWorkerOptions) { + static async create(options: CreateWorkerOptions): Promise { const { taskBroker, logger, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 700f2c6e6f..231c998f8b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -99,7 +99,7 @@ export async function createRouter( const workers = []; for (let i = 0; i < (taskWorkers || 1); i++) { - const worker = TaskWorker.createWorker({ + const worker = await TaskWorker.create({ taskBroker, actionRegistry, integrations, From ab6b7191c55a144dedf6b94448314bf4d672b882 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 13:08:13 +0100 Subject: [PATCH 170/245] change test to await the task worker create Signed-off-by: Brian Fletcher --- .../src/scaffolder/tasks/TaskWorker.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index f33b9182ce..9b6baf3b84 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -82,7 +82,7 @@ describe('TaskWorker', () => { it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = TaskWorker.create({ + const taskWorker = await TaskWorker.create({ logger, workingDirectory, integrations, @@ -113,7 +113,7 @@ describe('TaskWorker', () => { it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = TaskWorker.create({ + const taskWorker = await TaskWorker.create({ logger, workingDirectory, integrations, @@ -142,7 +142,7 @@ describe('TaskWorker', () => { }); const broker = new StorageTaskBroker(storage, logger); - const taskWorker = TaskWorker.create({ + const taskWorker = await TaskWorker.create({ logger, workingDirectory, integrations, From 93d0ea461258ffe09070ef7088cbf9baf2bee0bd Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 13:18:12 +0100 Subject: [PATCH 171/245] add api reports changes Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 5441c4de6b..6f9822cd09 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -523,7 +523,7 @@ export type TaskStoreListEventsOptions = { // @public export class TaskWorker { // (undocumented) - static createWorker(options: CreateWorkerOptions): TaskWorker; + static create(options: CreateWorkerOptions): Promise; // (undocumented) runOneTask(task: TaskContext): Promise; // (undocumented) From 106a5dc3ad6efc9ecf212f9b5dd3cee080aba463 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 27 Oct 2021 14:29:26 +0200 Subject: [PATCH 172/245] Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator` Signed-off-by: Oliver Sand --- .changeset/techdocs-poor-paws-rest.md | 6 ++++ .../search/DefaultTechDocsCollator.test.ts | 4 +++ .../src/search/DefaultTechDocsCollator.ts | 4 ++- .../reader/components/TechDocsSearch.test.tsx | 34 ++++-------------- .../src/reader/components/TechDocsSearch.tsx | 36 +++++-------------- 5 files changed, 28 insertions(+), 56 deletions(-) create mode 100644 .changeset/techdocs-poor-paws-rest.md diff --git a/.changeset/techdocs-poor-paws-rest.md b/.changeset/techdocs-poor-paws-rest.md new file mode 100644 index 0000000000..71e00f18ba --- /dev/null +++ b/.changeset/techdocs-poor-paws-rest.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 29c74a119c..21be228636 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -138,6 +138,8 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { componentType: entity!.spec!.type, lifecycle: entity!.spec!.lifecycle, owner: '', + kind: entity.kind, + name: entity.metadata.name, }); }); }); @@ -183,6 +185,8 @@ describe('DefaultTechDocsCollator', () => { componentType: entity!.spec!.type, lifecycle: entity!.spec!.lifecycle, owner: '', + kind: entity.kind, + name: entity.metadata.name, }); }); }); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index c0a96b7862..121ef6e46d 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -130,7 +130,9 @@ export class DefaultTechDocsCollator implements DocumentCollator { path: doc.location, }), path: doc.location, - ...entityInfo, + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, entityTitle: entity.metadata.title, componentType: entity.spec?.type?.toString() || 'other', lifecycle: (entity.spec?.lifecycle as string) || '', diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx index c6f017e92d..602a94e758 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { buildInitialFilters, TechDocsSearch } from './TechDocsSearch'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { searchApiRef } from '@backstage/plugin-search'; +import { wrapInTestApp } from '@backstage/test-utils'; import { act, fireEvent, @@ -22,9 +23,8 @@ import { waitFor, within, } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { searchApiRef } from '@backstage/plugin-search'; +import React from 'react'; +import { TechDocsSearch } from './TechDocsSearch'; const entityId = { name: 'test', @@ -89,7 +89,7 @@ describe('', () => { await singleResult; expect(querySpy).toBeCalledWith({ filters: { - kind: 'testable', + kind: 'Testable', name: 'test', namespace: 'testspace', }, @@ -108,7 +108,7 @@ describe('', () => { await waitFor(() => expect(querySpy).toBeCalledWith({ filters: { - kind: 'testable', + kind: 'Testable', name: 'test', namespace: 'testspace', }, @@ -120,23 +120,3 @@ describe('', () => { }); }); }); - -describe('buildInitialFilters', () => { - const filterEnt = { - name: 'Test', - kind: 'TestKind', - namespace: 'TeStNaMeSpAcE', - }; - it('should use filters as is when legacy path', () => { - const filters = buildInitialFilters(true, filterEnt); - expect(filters).toStrictEqual(filterEnt); - }); - it('should lowercase all filters for new approach', () => { - const filters = buildInitialFilters(false, filterEnt); - expect(filters).toStrictEqual({ - name: 'test', - kind: 'testkind', - namespace: 'testnamespace', - }); - }); -}); diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.tsx b/plugins/techdocs/src/reader/components/TechDocsSearch.tsx index 64229e6620..5bc1947ed6 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsSearch.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import React, { ChangeEvent, useEffect, useState } from 'react'; +import { EntityName } from '@backstage/catalog-model'; +import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; import { CircularProgress, Grid, @@ -22,21 +23,15 @@ import { InputAdornment, TextField, } from '@material-ui/core'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; -import { DocsResultListItem } from '../../components/DocsResultListItem'; import SearchIcon from '@material-ui/icons/Search'; -import { useDebounce } from 'react-use'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { ChangeEvent, useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { useDebounce } from 'react-use'; +import { DocsResultListItem } from '../../components/DocsResultListItem'; -type EntityId = { - name: string; - namespace: string; - kind: string; -}; type TechDocsSearchProps = { - entityId: EntityId; + entityId: EntityName; debounceTime?: number; }; @@ -54,17 +49,6 @@ type TechDocsSearchResult = { document: TechDocsDoc; }; -export const buildInitialFilters = ( - legacyPaths: boolean, - entityId: EntityId, -) => { - return legacyPaths - ? entityId - : Object.entries(entityId).reduce((acc, [key, value]) => { - return { ...acc, [key]: value?.toLocaleLowerCase('en-US') }; - }, {}); -}; - const TechDocsSearchBar = ({ entityId, debounceTime = 150, @@ -176,15 +160,11 @@ const TechDocsSearchBar = ({ }; export const TechDocsSearch = (props: TechDocsSearchProps) => { - const configApi = useApi(configApiRef); - const legacyPaths = configApi.getOptionalBoolean( - 'techdocs.legacyUseCaseSensitiveTripletPaths', - ); const initialState = { term: '', types: ['techdocs'], pageCursor: '', - filters: buildInitialFilters(legacyPaths || false, props.entityId), + filters: props.entityId, }; return ( From 6364c3d6ffd438eb899a438e88c89dd104f942f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:43:19 +0000 Subject: [PATCH 173/245] build(deps): bump @spotify/eslint-config-base from 9.0.2 to 12.0.0 Bumps [@spotify/eslint-config-base](https://github.com/spotify/web-scripts) from 9.0.2 to 12.0.0. - [Release notes](https://github.com/spotify/web-scripts/releases) - [Changelog](https://github.com/spotify/web-scripts/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotify/web-scripts/compare/v9.0.2...v12.0.0) --- updated-dependencies: - dependency-name: "@spotify/eslint-config-base" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ef95b6efda..fa42c8520b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,7 +41,7 @@ "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-yaml": "^3.0.0", - "@spotify/eslint-config-base": "^9.0.0", + "@spotify/eslint-config-base": "^12.0.0", "@spotify/eslint-config-react": "^10.0.0", "@spotify/eslint-config-typescript": "^10.0.0", "@sucrase/jest-plugin": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index f7cdd98a03..3164cdcfc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5408,10 +5408,10 @@ resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@spotify/eslint-config-base@^9.0.0": - version "9.0.2" - resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-9.0.2.tgz#a4830f610f40de935de795d3def486c3e5064ee4" - integrity sha512-HS+eLpVMbUUBXZ9ZlPcd6yCje/OVNhdcGDeU+DoK+C9AFfzbRtuZ1EJ8ewzj58ENrhlfJPuxCS68bypA/esyjw== +"@spotify/eslint-config-base@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-12.0.0.tgz#0b1e41bb436d5c1c20714703629514d64c3c0f06" + integrity sha512-5Uud/TmzakqmdUNCZpD8JFQRa2VG3dVd3DanSMpU/nVdu6K5LyX8EMU3Tz1vGP18Wih8iAu/sBSJhntNzw7e6w== "@spotify/eslint-config-react@^10.0.0": version "10.0.0" From 563212e5dd8e52ace7c84fb9e66bd347ddcc5c61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Oct 2021 07:44:09 +0000 Subject: [PATCH 174/245] build(deps): bump @types/dockerode from 3.2.7 to 3.3.0 Bumps [@types/dockerode](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/dockerode) from 3.2.7 to 3.3.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/dockerode) --- updated-dependencies: - dependency-name: "@types/dockerode" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index 004af73f17..19a490f5ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6795,9 +6795,9 @@ "@types/ssh2" "*" "@types/dockerode@^3.2.1": - version "3.2.7" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.7.tgz#0bc1f583b0e1edc4ca0c6179ef251382df35dcb3" - integrity sha512-Y8hMRQTwsOjz4qm6yilZKKjB/Y7+2EOiY3RPN1Xtu63wEUEDVv+3Ou+sgiisPE9+pVe3bmwhnF+E1Iwj/o4J6w== + version "3.3.0" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz#d6afcc1de31e211ee900da3a48ef4f1a496cf05a" + integrity sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg== dependencies: "@types/docker-modem" "*" "@types/node" "*" @@ -7288,10 +7288,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": - version "14.17.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" - integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^15.6.1": + version "15.14.9" + resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" + integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/node@10.17.13", "@types/node@^10.1.0", "@types/node@^10.12.0": version "10.17.13" @@ -7303,10 +7303,10 @@ resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== -"@types/node@^15.6.1": - version "15.14.9" - resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" - integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== +"@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": + version "14.17.8" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" + integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== "@types/normalize-package-data@^2.4.0": version "2.4.0" From 1abc9bab308eb108b099dd04f2efb9a983d68e91 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Oct 2021 14:48:56 +0200 Subject: [PATCH 175/245] chore: updating to work with the new @types/node Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 9 ++++++-- yarn.lock | 32 ++++++++++++++++++--------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index b0db715f25..5d4100e33c 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -350,6 +350,7 @@ async function testAppServe(pluginName: string, appDir: string) { GITHUB_TOKEN: 'abc', }, }); + Browser.localhost('localhost', 3000); let successful = false; @@ -378,7 +379,9 @@ async function testAppServe(pluginName: string, appDir: string) { } } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes - killTree(startApp.pid); + if (startApp.pid) { + killTree(startApp.pid); + } } try { @@ -457,7 +460,9 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { } finally { print('Stopping the child process'); // Kill entire process group, otherwise we'll end up with hanging serve processes - killTree(child.pid); + if (child.pid) { + killTree(child.pid); + } } try { diff --git a/yarn.lock b/yarn.lock index 19a490f5ce..c3c8ebde07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7288,25 +7288,35 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^15.6.1": - version "15.14.9" - resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" - integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "16.11.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@types/node@10.17.13", "@types/node@^10.1.0", "@types/node@^10.12.0": +"@types/node@10.17.13": version "10.17.13" resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== +"@types/node@^10.1.0", "@types/node@^10.12.0": + version "10.17.60" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== + "@types/node@^12.7.1": - version "12.12.58" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" - integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== + version "12.20.36" + resolved "https://registry.npmjs.org/@types/node/-/node-12.20.36.tgz#5bd54d2383e714fc4d2c258107ee70c5bad86d0c" + integrity sha512-+5haRZ9uzI7rYqzDznXgkuacqb6LJhAti8mzZKWxIXn/WEtvB+GHVJ7AuMwcN1HMvXOSJcrvA6PPoYHYOYYebA== "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": - version "14.17.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" - integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== + version "14.17.32" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" + integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== + +"@types/node@^15.6.1": + version "15.14.9" + resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" + integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/normalize-package-data@^2.4.0": version "2.4.0" From ef9dd217b6e7fac534340fa209fed055d24c99a8 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Oct 2021 15:14:02 +0200 Subject: [PATCH 176/245] chore: reverting the node types Signed-off-by: blam --- yarn.lock | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index c3c8ebde07..a2ea5b777d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7288,30 +7288,20 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0": - version "16.11.6" - resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": + version "14.17.8" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" + integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== -"@types/node@10.17.13": +"@types/node@10.17.13", "@types/node@^10.1.0", "@types/node@^10.12.0": version "10.17.13" resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== -"@types/node@^10.1.0", "@types/node@^10.12.0": - version "10.17.60" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - "@types/node@^12.7.1": - version "12.20.36" - resolved "https://registry.npmjs.org/@types/node/-/node-12.20.36.tgz#5bd54d2383e714fc4d2c258107ee70c5bad86d0c" - integrity sha512-+5haRZ9uzI7rYqzDznXgkuacqb6LJhAti8mzZKWxIXn/WEtvB+GHVJ7AuMwcN1HMvXOSJcrvA6PPoYHYOYYebA== - -"@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": - version "14.17.32" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" - integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== + version "12.12.58" + resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" + integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== "@types/node@^15.6.1": version "15.14.9" From 139f4b1f39e5d6409287fa617796954ed3ce36b7 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Oct 2021 15:14:37 +0200 Subject: [PATCH 177/245] chore: revert the e2e Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 5d4100e33c..b0db715f25 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -350,7 +350,6 @@ async function testAppServe(pluginName: string, appDir: string) { GITHUB_TOKEN: 'abc', }, }); - Browser.localhost('localhost', 3000); let successful = false; @@ -379,9 +378,7 @@ async function testAppServe(pluginName: string, appDir: string) { } } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes - if (startApp.pid) { - killTree(startApp.pid); - } + killTree(startApp.pid); } try { @@ -460,9 +457,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { } finally { print('Stopping the child process'); // Kill entire process group, otherwise we'll end up with hanging serve processes - if (child.pid) { - killTree(child.pid); - } + killTree(child.pid); } try { From 2fdf3119d33a39f6d963477e7f548eb751efa03f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 14:32:49 +0000 Subject: [PATCH 178/245] build(deps): bump typescript-json-schema from 0.50.1 to 0.51.0 Bumps [typescript-json-schema](https://github.com/YousefED/typescript-json-schema) from 0.50.1 to 0.51.0. - [Release notes](https://github.com/YousefED/typescript-json-schema/releases) - [Commits](https://github.com/YousefED/typescript-json-schema/compare/v0.50.1...v0.51.0) --- updated-dependencies: - dependency-name: typescript-json-schema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- packages/config-loader/package.json | 2 +- yarn.lock | 94 ++++++++++++++++++----------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index a731225e2b..2d15c31438 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -41,7 +41,7 @@ "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", - "typescript-json-schema": "^0.50.1", + "typescript-json-schema": "^0.51.0", "yaml": "^1.9.2", "yup": "^0.32.9" }, diff --git a/yarn.lock b/yarn.lock index 004af73f17..20975100ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2602,6 +2602,18 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -6486,10 +6498,10 @@ resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== -"@tsconfig/node16@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1" - integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA== +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== "@types/accepts@*", "@types/accepts@^1.3.5": version "1.3.5" @@ -7110,7 +7122,7 @@ dependencies: "@types/json-schema" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -7288,10 +7300,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": - version "14.17.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" - integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^16.9.2": + version "16.11.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/node@10.17.13", "@types/node@^10.1.0", "@types/node@^10.12.0": version "10.17.13" @@ -7303,6 +7315,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== +"@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": + version "14.17.8" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" + integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== + "@types/node@^15.6.1": version "15.14.9" resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" @@ -8411,6 +8428,11 @@ acorn-walk@^7.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^5.5.3: version "5.7.4" resolved "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" @@ -27098,23 +27120,25 @@ ts-log@^2.2.3: resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb" integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== -ts-node@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be" - integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg== +ts-node@^10.0.0, ts-node@^10.2.1: + version "10.4.0" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== dependencies: + "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" "@tsconfig/node12" "^1.0.7" "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.1" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" arg "^4.1.0" create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.17" yn "3.1.1" -ts-node@^9, ts-node@^9.1.1: +ts-node@^9: version "9.1.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== @@ -27315,29 +27339,29 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-json-schema@^0.50.1: - version "0.50.1" - resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.50.1.tgz#48041eb9f6efbdf4ba88c3e3af9433601f7a2b47" - integrity sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA== +typescript-json-schema@^0.51.0: + version "0.51.0" + resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.51.0.tgz#e2abff69b8564c98c0edef2c13d55ef10fd71427" + integrity sha512-POhWbUNs2oaBti1W9k/JwS+uDsaZD9J/KQiZ/iXRQEOD0lTn9VmshIls9tn+A9X6O+smPjeEz5NEy6WTkCCzrQ== dependencies: - "@types/json-schema" "^7.0.7" - "@types/node" "^14.14.33" - glob "^7.1.6" + "@types/json-schema" "^7.0.9" + "@types/node" "^16.9.2" + glob "^7.1.7" json-stable-stringify "^1.0.1" - ts-node "^9.1.1" + ts-node "^10.2.1" typescript "~4.2.3" - yargs "^16.2.0" + yargs "^17.1.1" -typescript@^4.0.3, typescript@~4.2.3: - version "4.2.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== - -typescript@~4.3.5: +typescript@^4.0.3, typescript@~4.3.5: version "4.3.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@~4.2.3: + version "4.2.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" + integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" @@ -28906,10 +28930,10 @@ yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.0.1: - version "17.0.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" - integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== +yargs@^17.0.0, yargs@^17.0.1, yargs@^17.1.1: + version "17.2.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" + integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q== dependencies: cliui "^7.0.2" escalade "^3.1.1" From 4a8a1a8bbe7e9446cb8bc0e9e142896a69c12dc9 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Oct 2021 15:25:40 +0200 Subject: [PATCH 179/245] chore: fix up the types for this dependabot bump Signed-off-by: blam --- yarn.lock | 68 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 20975100ec..cb9eda94b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6498,6 +6498,11 @@ resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== +"@tsconfig/node16@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1" + integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA== + "@tsconfig/node16@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" @@ -7300,10 +7305,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^16.9.2": - version "16.11.6" - resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": + version "14.17.8" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" + integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== "@types/node@10.17.13", "@types/node@^10.1.0", "@types/node@^10.12.0": version "10.17.13" @@ -7315,16 +7320,16 @@ resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== -"@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": - version "14.17.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" - integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== - "@types/node@^15.6.1": version "15.14.9" resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== +"@types/node@^16.9.2": + version "16.11.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -27120,7 +27125,23 @@ ts-log@^2.2.3: resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb" integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== -ts-node@^10.0.0, ts-node@^10.2.1: +ts-node@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be" + integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg== + dependencies: + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + +ts-node@^10.2.1: version "10.4.0" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== @@ -27352,16 +27373,16 @@ typescript-json-schema@^0.51.0: typescript "~4.2.3" yargs "^17.1.1" -typescript@^4.0.3, typescript@~4.3.5: - version "4.3.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== - -typescript@~4.2.3: +typescript@^4.0.3, typescript@~4.2.3: version "4.2.4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== +typescript@~4.3.5: + version "4.3.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" @@ -28930,7 +28951,20 @@ yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.0.1, yargs@^17.1.1: +yargs@^17.0.0, yargs@^17.0.1: + version "17.0.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" + integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.1.1: version "17.2.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q== From ea21f7f567f1652898c90bb63c55f5776822b821 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Oct 2021 15:27:40 +0200 Subject: [PATCH 180/245] chore: add changeset Signed-off-by: blam --- .changeset/wise-lamps-appear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-lamps-appear.md diff --git a/.changeset/wise-lamps-appear.md b/.changeset/wise-lamps-appear.md new file mode 100644 index 0000000000..4189e55d49 --- /dev/null +++ b/.changeset/wise-lamps-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +bump `typescript-json-schema` from 0.50.1 to 0.51.0 From b0dc1fd241f519101bd9a6262eef21460dab541c Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 27 Oct 2021 15:32:09 +0200 Subject: [PATCH 181/245] add changeset Signed-off-by: Ben Lambert --- .changeset/green-jeans-beg.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/green-jeans-beg.md diff --git a/.changeset/green-jeans-beg.md b/.changeset/green-jeans-beg.md new file mode 100644 index 0000000000..bafc33fad9 --- /dev/null +++ b/.changeset/green-jeans-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +bump `@spotify/eslint-config-base` from 9.0.2 to 12.0.0 From 806c2a7568d0ef02e0968861454b99b25b3554a6 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 15:08:03 +0100 Subject: [PATCH 182/245] removes unused d3-zoom dep Signed-off-by: Brian Fletcher --- packages/core-components/package.json | 2 -- yarn.lock | 22 +--------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 865f5b6df9..c0ecbf0245 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -44,7 +44,6 @@ "clsx": "^1.1.0", "d3-selection": "^2.0.0", "d3-shape": "^2.0.0", - "d3-zoom": "^2.0.0", "dagre": "^0.8.5", "immer": "^9.0.1", "lodash": "^4.17.21", @@ -77,7 +76,6 @@ "@types/classnames": "^2.2.9", "@types/d3-selection": "^2.0.0", "@types/d3-shape": "^3.0.1", - "@types/d3-zoom": "^2.0.0", "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", diff --git a/yarn.lock b/yarn.lock index 004af73f17..9c1f3da635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6715,23 +6715,11 @@ dependencies: postcss "5 - 7" -"@types/d3-color@*": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.0.tgz#febdfadade56e215a4c3f612fe3000d92999f5d5" - integrity sha512-Bs0maTeU47rdZT+n42iQ0C4gnbnJlIDJkqHFtIsDx2tPPITDeoSdIrm+00UYXzegzArYC2GsG80eHNMwz08IAw== - "@types/d3-force@^2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" integrity sha512-3r+CQv2K/uDTAVg0DGxsbBjV02vgOxb8RhPIv3gd6cp3pdPAZ7wEXpDjUZSoqycAQLSDOxG/AZ54Vx6YXZSbmQ== -"@types/d3-interpolate@*": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.0.tgz#325029216dc722c1c68c33ccda759f1209d35823" - integrity sha512-Wt1v2zTlEN8dSx8hhx6MoOhWQgTkz0Ukj7owAEIOF2QtI0e219paFX9rf/SLOr/UExWb1TcUzatU8zWwFby6gg== - dependencies: - "@types/d3-color" "*" - "@types/d3-path@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" @@ -6742,7 +6730,7 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== -"@types/d3-selection@*", "@types/d3-selection@^2.0.0": +"@types/d3-selection@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.0.tgz#59df94a8e47ed1050a337d4ffb4d4d213aa590a8" integrity sha512-EF0lWZ4tg7oDFg4YQFlbOU3936e3a9UmoQ2IXlBy1+cv2c2Pv7knhKUzGlH5Hq2sF/KeDTH1amiRPey2rrLMQA== @@ -6761,14 +6749,6 @@ dependencies: "@types/d3-path" "*" -"@types/d3-zoom@^2.0.0": - version "2.0.1" - resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.1.tgz#6f0d993042124947314053c937784e24d8b003cf" - integrity sha512-FuiGLfaHmp84b9wsj0dG03E/aJl5k98OLnJ2/5p7bQOHEpWqR+z5WCoWYMAbdGxaca7VNd9tCT5i6AJnpauNTQ== - dependencies: - "@types/d3-interpolate" "*" - "@types/d3-selection" "*" - "@types/dagre@^0.7.44": version "0.7.44" resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.44.tgz#8f4b796b118ca29c132da7068fbc0d0351ee5851" From 494003b68e41481cfa42515055680c5b8cb1cd74 Mon Sep 17 00:00:00 2001 From: Ray Sinnema Date: Tue, 26 Oct 2021 18:16:05 +0200 Subject: [PATCH 183/245] Reduce diff Signed-off-by: Ray Sinnema --- ADOPTERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 922d2d98e7..c00625e39c 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -58,5 +58,5 @@ | [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | | [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | | [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | -| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | | [Adevinta](https://www.adevinta.com/) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to internal customers. | From afd5c82ce1e4d25a7e2692266e1d8525ccd9b35b Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 15:14:45 +0100 Subject: [PATCH 184/245] adds a changeset file Signed-off-by: Brian Fletcher --- .changeset/odd-mirrors-drum.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/odd-mirrors-drum.md diff --git a/.changeset/odd-mirrors-drum.md b/.changeset/odd-mirrors-drum.md new file mode 100644 index 0000000000..c49c09dd1a --- /dev/null +++ b/.changeset/odd-mirrors-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removes the unused d3-zoom dependency From 96cfa561ebd00de4739ca7a6a6c9adc612497f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Oct 2021 16:25:23 +0200 Subject: [PATCH 185/245] Just some random improvements in backend-common too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/modern-elephants-applaud.md | 5 + packages/backend-common/api-report.md | 14 +- packages/backend-common/src/reading/index.ts | 1 + packages/backend-common/src/reading/types.ts | 143 ++++++++++++++----- 4 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 .changeset/modern-elephants-applaud.md diff --git a/.changeset/modern-elephants-applaud.md b/.changeset/modern-elephants-applaud.md new file mode 100644 index 0000000000..0f71bd7313 --- /dev/null +++ b/.changeset/modern-elephants-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Adjusted some API exports diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2336a57ec3..a19447034e 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -211,6 +211,12 @@ export type ErrorHandlerOptions = { logClientErrors?: boolean; }; +// @public +export type FromReadableArrayOptions = Array<{ + data: Readable; + path: string; +}>; + // @public (undocumented) export function getRootLogger(): winston.Logger; @@ -419,15 +425,13 @@ export type ReadTreeResponse = { etag: string; }; -// @public (undocumented) +// @public export type ReadTreeResponseDirOptions = { targetDir?: string; }; // @public (undocumented) export interface ReadTreeResponseFactory { - // Warning: (ae-forgotten-export) The symbol "FromReadableArrayOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) fromReadableArray( options: FromReadableArrayOptions, @@ -442,7 +446,7 @@ export interface ReadTreeResponseFactory { ): Promise; } -// @public (undocumented) +// @public export type ReadTreeResponseFactoryOptions = { stream: Readable; subpath?: string; @@ -580,7 +584,7 @@ export type UrlReader = { search(url: string, options?: SearchOptions): Promise; }; -// @public (undocumented) +// @public export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; reader: UrlReader; diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 77096795ba..2c3394e4fc 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -20,6 +20,7 @@ export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export { AwsS3UrlReader } from './AwsS3UrlReader'; export type { + FromReadableArrayOptions, ReaderFactory, ReadTreeOptions, ReadTreeResponse, diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index ff799f548a..7e0904f18b 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -24,24 +24,41 @@ import { Config } from '@backstage/config'; * @public */ export type UrlReader = { - /* Used to read a single file and return its content. */ + /** + * Reads a single file and return its content. + */ read(url: string): Promise; /** - * A replacement for the read method that supports options and complex responses. + * Reads a single file and return its content. * - * Use this whenever it is available, as the read method will be deprecated and - * eventually removed in the future. + * @remarks + * + * This is a replacement for the read method that supports options and + * complex responses. + * + * Use this whenever it is available, as the read method will be + * deprecated and eventually removed in a future release. */ readUrl?(url: string, options?: ReadUrlOptions): Promise; - /* Used to read a file tree and download as a directory. */ + /** + * Reads a full or partial file tree. + */ readTree(url: string, options?: ReadTreeOptions): Promise; - /* Used to search a file in a tree using a glob pattern. */ + + /** + * Searches for a file in a tree using a glob pattern. + */ search(url: string, options?: SearchOptions): Promise; }; -/** @public */ +/** + * A predicate that decides whether a specific {@link UrlReader} can handle a + * given URL. + * + * @public + */ export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; reader: UrlReader; @@ -49,7 +66,7 @@ export type UrlReaderPredicateTuple = { /** * A factory function that can read config to construct zero or more - * UrlReaders along with a predicate for when it should be used. + * {@link UrlReader}s along with a predicate for when it should be used. * * @public */ @@ -66,21 +83,28 @@ export type ReaderFactory = (options: { */ export type ReadUrlOptions = { /** - * An etag can be provided to check whether readUrl's response has changed from a previous execution. + * An ETag which can be provided to check whether a + * {@link UrlReader.readUrl} response has changed from a previous execution. * - * In the readUrl() response, an etag is returned along with the data. The etag is a unique identifer - * of the data, usually the commit SHA or etag from the target. + * @remarks * - * When an etag is given in ReadUrlOptions, readUrl will first compare the etag against the etag - * on the target. If they match, readUrl will throw a NotModifiedError indicating that the readUrl - * response will not differ from the previous response which included this particular etag. If they - * do not match, readUrl will return the rest of ReadUrlResponse along with a new etag. + * In the {@link UrlReader.readUrl} response, an ETag is returned along with + * the data. The ETag is a unique identifier of the data, usually the commit + * SHA or ETag from the target. + * + * When an ETag is given in ReadUrlOptions, {@link UrlReader.readUrl} will + * first compare the ETag against the ETag of the target. If they match, + * {@link UrlReader.readUrl} will throw a + * {@link @backstage/errors#NotModifiedError} indicating that the response + * will not differ from the previous response which included this particular + * ETag. If they do not match, {@link UrlReader.readUrl} will return the rest + * of the response along with a new ETag. */ etag?: string; }; /** - * A response object for readUrl operations. + * A response object for {@link UrlReader.readUrl} operations. * * @public */ @@ -92,13 +116,16 @@ export type ReadUrlResponse = { /** * Etag returned by content provider. + * + * @remarks + * * Can be used to compare and cache responses when doing subsequent calls. */ etag?: string; }; /** - * An options object for readTree operations. + * An options object for {@link UrlReader.readTree} operations. * * @public */ @@ -106,6 +133,8 @@ export type ReadTreeOptions = { /** * A filter that can be used to select which files should be included. * + * @remarks + * * The path passed to the filter function is the relative path from the URL * that the file tree is fetched from, without any leading '/'. * @@ -113,56 +142,82 @@ export type ReadTreeOptions = { * at https://github.com/my/repo/blob/master/my-dir/my-subdir/my-file.txt will * be represented as my-subdir/my-file.txt * - * If no filter is provided all files are extracted. + * If no filter is provided, all files are extracted. */ filter?(path: string, info?: { size: number }): boolean; /** - * An etag can be provided to check whether readTree's response has changed from a previous execution. + * An ETag which can be provided to check whether a + * {@link UrlReader.readTree} response has changed from a previous execution. * - * In the readTree() response, an etag is returned along with the tree blob. The etag is a unique identifer - * of the tree blob, usually the commit SHA or etag from the target. + * @remarks * - * When an etag is given in ReadTreeOptions, readTree will first compare the etag against the etag - * on the target branch. If they match, readTree will throw a NotModifiedError indicating that the readTree - * response will not differ from the previous response which included this particular etag. If they - * do not match, readTree will return the rest of ReadTreeResponse along with a new etag. + * In the {@link UrlReader.readTree} response, an ETag is returned along with + * the tree blob. The ETag is a unique identifier of the tree blob, usually + * the commit SHA or ETag from the target. + * + * When an ETag is given as a request option, {@link UrlReader.readTree} will + * first compare the ETag against the ETag on the target branch. If they + * match, {@link UrlReader.readTree} will throw a + * {@link @backstage/errors#NotModifiedError} indicating that the response + * will not differ from the previous response which included this particular + * ETag. If they do not match, {@link UrlReader.readTree} will return the + * rest of the response along with a new ETag. */ etag?: string; }; -/** @public */ +/** + * Options that control {@link ReadTreeResponse.dir} execution. + * + * @public + */ export type ReadTreeResponseDirOptions = { - /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ + /** + * The directory to write files to. + * + * @remarks + * + * Defaults to the OS tmpdir, or `backend.workingDirectory` if set in config. + */ targetDir?: string; }; /** - * A response object for readTree operations. + * A response object for {@link UrlReader.readTree} operations. * * @public */ export type ReadTreeResponse = { /** - * files() returns an array of all the files inside the tree and corresponding functions to read their content. + * Returns an array of all the files inside the tree, and corresponding + * functions to read their content. */ files(): Promise; + + /** + * Returns the tree contents as a binary archive, using a stream. + */ archive(): Promise; /** - * dir() extracts the tree response into a directory and returns the path of the directory. + * Extracts the tree response into a directory and returns the path of the + * directory. */ dir(options?: ReadTreeResponseDirOptions): Promise; /** * Etag returned by content provider. + * + * @remarks + * * Can be used to compare and cache responses when doing subsequent calls. */ etag: string; }; /** - * Represents a single file in a readTree response. + * Represents a single file in a {@link UrlReader.readTree} response. * * @public */ @@ -171,7 +226,11 @@ export type ReadTreeResponseFile = { content(): Promise; }; -/** @public */ +/** + * Options that control execution of {@link ReadTreeResponseFactory} methods. + * + * @public + */ export type ReadTreeResponseFactoryOptions = { // A binary stream of a tar archive. stream: Readable; @@ -183,11 +242,21 @@ export type ReadTreeResponseFactoryOptions = { // Filter passed on from the ReadTreeOptions filter?: (path: string, info?: { size: number }) => boolean; }; -/** @public */ + +/** + * Options that control {@link ReadTreeResponseFactory.fromReadableArray} + * execution. + * + * @public + */ export type FromReadableArrayOptions = Array<{ - // Data in the form of a readable + /** + * The raw data itself. + */ data: Readable; - // A string containing the filepath of the data + /** + * The filepath of the data. + */ path: string; }>; @@ -213,7 +282,7 @@ export type SearchOptions = { /** * An etag can be provided to check whether the search response has changed from a previous execution. * - * In the search() response, an etag is returned along with the files. The etag is a unique identifer + * In the search() response, an etag is returned along with the files. The etag is a unique identifier * of the current tree, usually the commit SHA or etag from the target. * * When an etag is given in SearchOptions, search will first compare the etag against the etag @@ -236,7 +305,7 @@ export type SearchResponse = { files: SearchResponseFile[]; /** - * A unique identifer of the current remote tree, usually the commit SHA or etag from the target. + * A unique identifier of the current remote tree, usually the commit SHA or etag from the target. */ etag: string; }; From 54dac6d2d153ef3b867b03b73d0f08542750d695 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 27 Oct 2021 15:38:38 +0100 Subject: [PATCH 186/245] re-introduce updated version of d3 libs Signed-off-by: Brian Fletcher --- .changeset/odd-mirrors-drum.md | 2 +- packages/core-components/package.json | 8 +- yarn.lock | 102 +++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/.changeset/odd-mirrors-drum.md b/.changeset/odd-mirrors-drum.md index c49c09dd1a..290ba7c526 100644 --- a/.changeset/odd-mirrors-drum.md +++ b/.changeset/odd-mirrors-drum.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Removes the unused d3-zoom dependency +Updates the d3 dependencies diff --git a/packages/core-components/package.json b/packages/core-components/package.json index c0ecbf0245..038b99ca90 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -42,8 +42,9 @@ "@types/react-text-truncate": "^0.14.0", "classnames": "^2.2.6", "clsx": "^1.1.0", - "d3-selection": "^2.0.0", - "d3-shape": "^2.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.0.0", + "d3-zoom": "^3.0.0", "dagre": "^0.8.5", "immer": "^9.0.1", "lodash": "^4.17.21", @@ -74,8 +75,9 @@ "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.1.8", "@types/classnames": "^2.2.9", - "@types/d3-selection": "^2.0.0", + "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", + "@types/d3-zoom": "^3.0.1", "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", diff --git a/yarn.lock b/yarn.lock index 9c1f3da635..28f3a4517c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6715,11 +6715,23 @@ dependencies: postcss "5 - 7" +"@types/d3-color@*": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" + integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== + "@types/d3-force@^2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" integrity sha512-3r+CQv2K/uDTAVg0DGxsbBjV02vgOxb8RhPIv3gd6cp3pdPAZ7wEXpDjUZSoqycAQLSDOxG/AZ54Vx6YXZSbmQ== +"@types/d3-interpolate@*": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== + dependencies: + "@types/d3-color" "*" + "@types/d3-path@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" @@ -6730,10 +6742,10 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== -"@types/d3-selection@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.0.tgz#59df94a8e47ed1050a337d4ffb4d4d213aa590a8" - integrity sha512-EF0lWZ4tg7oDFg4YQFlbOU3936e3a9UmoQ2IXlBy1+cv2c2Pv7knhKUzGlH5Hq2sF/KeDTH1amiRPey2rrLMQA== +"@types/d3-selection@*", "@types/d3-selection@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.1.tgz#e57b01ab69b18b380f68db97b76ceefe62f17191" + integrity sha512-aJ1d1SCUtERHH65bB8NNoLpUOI3z8kVcfg2BGm4rMMUwuZF4x6qnIEKjT60Vt0o7gP/a/xkRVs4D9CpDifbyRA== "@types/d3-shape@^1": version "1.3.5" @@ -6749,6 +6761,14 @@ dependencies: "@types/d3-path" "*" +"@types/d3-zoom@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" + integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + "@types/dagre@^0.7.44": version "0.7.44" resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.44.tgz#8f4b796b118ca29c132da7068fbc0d0351ee5851" @@ -12022,11 +12042,21 @@ d3-color@1: resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== +"d3-color@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" + integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== + "d3-dispatch@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + d3-drag@2: version "2.0.0" resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz#9eaf046ce9ed1c25c88661911c1d5a4d8eb7ea6d" @@ -12035,11 +12065,24 @@ d3-drag@2: d3-dispatch "1 - 2" d3-selection "2" +"d3-drag@2 - 3": + version "3.0.0" + resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + "d3-ease@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz#fd1762bfca00dae4bacea504b1d628ff290ac563" integrity sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ== +"d3-ease@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + d3-force@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz#f20ccbf1e6c9e80add1926f09b51f686a8bc0937" @@ -12068,6 +12111,13 @@ d3-interpolate@1, d3-interpolate@^1.3.0: dependencies: d3-color "1 - 2" +"d3-interpolate@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" @@ -12078,6 +12128,11 @@ d3-path@1: resolved "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== +"d3-path@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" + integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== + "d3-quadtree@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz#edbad045cef88701f6fee3aee8e93fb332d30f9d" @@ -12100,6 +12155,11 @@ d3-selection@2, d3-selection@^2.0.0: resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + d3-shape@^1.2.0: version "1.3.7" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" @@ -12114,6 +12174,13 @@ d3-shape@^2.0.0: dependencies: d3-path "1 - 2" +d3-shape@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.0.1.tgz#9ccdfb28fd9b0d12f2d8aec234cd5c4a9ea27931" + integrity sha512-HNZNEQoDhuCrDWEc/BMbF/hKtzMZVoe64TvisFLDp2Iyj0UShB/E6/lBsLlJTfBMbYgftHj90cXJ0SEitlE6Xw== + dependencies: + d3-path "1 - 3" + d3-time-format@2: version "2.3.0" resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" @@ -12131,6 +12198,11 @@ d3-time@1: resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + d3-transition@2: version "2.0.0" resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz#366ef70c22ef88d1e34105f507516991a291c94c" @@ -12142,6 +12214,17 @@ d3-transition@2: d3-interpolate "1 - 2" d3-timer "1 - 2" +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + d3-zoom@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz#f04d0afd05518becce879d04709c47ecd93fba54" @@ -12153,6 +12236,17 @@ d3-zoom@^2.0.0: d3-selection "2" d3-transition "2" +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" From 72e7a876840642ad8ae7295abcd86e46a537fe3a Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Wed, 27 Oct 2021 09:36:30 -0700 Subject: [PATCH 187/245] add more testing for FileExplorer tests Signed-off-by: Jeremy Guarini --- .../FileExplorer/FileExplorer.test.tsx | 144 +++++++++++++++++- .../components/FileExplorer/FileExplorer.tsx | 2 +- 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx index 948d9c9722..4163b22ac1 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { groupByPath } from './FileExplorer'; +import { groupByPath, buildFileStructure } from './FileExplorer'; const dummyFiles = [ { @@ -40,6 +40,22 @@ const dummyFiles = [ tracked: 1, path: '', }, + { + filename: 'dir3/dir2/dir1/file4', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + path: '', + }, + { + filename: 'dir3/dir2/dir3/file4', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + path: '', + }, ]; const dummyDataGroupedByPath = { @@ -71,6 +87,124 @@ const dummyDataGroupedByPath = { path: '', }, ], + dir3: [ + { + filename: 'dir3/dir2/dir1/file4', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + path: '', + }, + { + filename: 'dir3/dir2/dir3/file4', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + path: '', + }, + ], +}; + +const coverageTableRow = { + files: dummyFiles, + coverage: 1, + missing: 1, + tracked: 1, + path: '', +}; + +const coverageTableRowResults = { + files: [ + { + path: 'dir1', + files: [ + { + path: 'file1', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + }, + { + path: 'file2', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + }, + ], + coverage: 1, + missing: 2, + tracked: 2, + }, + { + path: 'dir2', + files: [ + { + path: 'file3', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + }, + ], + coverage: 1, + missing: 1, + tracked: 1, + }, + { + path: 'dir3', + files: [ + { + path: 'dir2', + files: [ + { + path: 'dir1', + files: [ + { + path: 'file4', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + }, + ], + coverage: 1, + missing: 1, + tracked: 1, + }, + { + path: 'dir3', + files: [ + { + path: 'file4', + files: [], + coverage: 1, + missing: 1, + tracked: 1, + }, + ], + coverage: 1, + missing: 1, + tracked: 1, + }, + ], + coverage: 1, + missing: 2, + tracked: 2, + }, + ], + coverage: 1, + missing: 2, + tracked: 2, + }, + ], + coverage: 1, + missing: 1, + tracked: 1, + path: '', }; describe('groupByPath function', () => { @@ -78,3 +212,11 @@ describe('groupByPath function', () => { expect(groupByPath(dummyFiles)).toStrictEqual(dummyDataGroupedByPath); }); }); + +describe('buildFileStructure function', () => { + it('should group files by their root directory,as per their filename', () => { + expect(buildFileStructure(coverageTableRow)).toStrictEqual( + coverageTableRowResults, + ); + }); +}); diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx index ed31cb9c62..49379551ad 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -82,7 +82,7 @@ const removeVisitedPathGroup = ( }); }; -const buildFileStructure = (row: CoverageTableRow) => { +export const buildFileStructure = (row: CoverageTableRow) => { const dataGroupedByPath: FileStructureObject = groupByPath(row.files); row.files = Object.keys(dataGroupedByPath).map(pathGroup => { return buildFileStructure({ From 767fe912b2be3ef2ec3fccfa7c68820b17de3bd1 Mon Sep 17 00:00:00 2001 From: Jeremy Guarini Date: Wed, 27 Oct 2021 09:47:30 -0700 Subject: [PATCH 188/245] add changeset Signed-off-by: Jeremy Guarini --- .changeset/weak-readers-know.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/weak-readers-know.md diff --git a/.changeset/weak-readers-know.md b/.changeset/weak-readers-know.md new file mode 100644 index 0000000000..24cf29a5db --- /dev/null +++ b/.changeset/weak-readers-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage': patch +--- + +Addresses bug when file path contains multiple directories with the same name. From 08de4fa957b9e18c37b5817c3a4324a78e8eb2e2 Mon Sep 17 00:00:00 2001 From: Thomas Viaud Date: Wed, 27 Oct 2021 17:38:37 +0100 Subject: [PATCH 189/245] fix: Adding corrected code documentation for custom field extension Signed-off-by: Thomas Viaud --- .../writing-custom-field-extensions.md | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 4f2d3bdf5d..1fb26b7977 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -27,20 +27,21 @@ You can create your own Field Extension by using the ```tsx //packages/app/scaffolder/MyCustomExtension/MyCustomExtension.tsx +import React from 'react'; import { FieldProps, FieldValidation } from '@rjsf/core'; +import FormControl from '@material-ui/core/FormControl'; import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; /* This is the actual component that will get rendered in the form */ -export const MyCustomExtension = ({ onChange, required }: FieldProps) => { - return ( - 0 && !formData} - onChange={onChange} - > - ) +export const MyCustomExtension = ({ onChange, rawErrors, required, formData }: FieldProps) => { + return ( + 0 && !formData} + onChange={onChange} /> + ) }; /* @@ -49,24 +50,24 @@ export const MyCustomExtension = ({ onChange, required }: FieldProps) => */ export const myCustomValidation = ( - value: string, - validation: FieldValidation, + value: string, + validation: FieldValidation, ) => { - if (!KubernetesValidatorFunctions.isValidObjectName(value)) { - validation.addError( - 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', - ); - } + if (!KubernetesValidatorFunctions.isValidObjectName(value)) { + validation.addError( + 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + ); + } }; ``` ```tsx // packages/app/scaffolder/MyCustomExtension/extensions.ts -/* +/* This is where the magic happens and creates the custom field extension. - Note that if you're writing extensions part of a separate plugin, + Note that if you're writing extensions part of a separate plugin, then please use `plugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`. */ From 224e545a755733eda489b07b5627c3a522d5498e Mon Sep 17 00:00:00 2001 From: Thomas Viaud Date: Wed, 27 Oct 2021 17:53:46 +0100 Subject: [PATCH 190/245] fix: run prettier Signed-off-by: Thomas Viaud --- .../writing-custom-field-extensions.md | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 1fb26b7977..ebb7af4398 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -34,14 +34,20 @@ import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; /* This is the actual component that will get rendered in the form */ -export const MyCustomExtension = ({ onChange, rawErrors, required, formData }: FieldProps) => { - return ( - 0 && !formData} - onChange={onChange} /> - ) +export const MyCustomExtension = ({ + onChange, + rawErrors, + required, + formData, +}: FieldProps) => { + return ( + 0 && !formData} + onChange={onChange} + /> + ); }; /* @@ -50,14 +56,14 @@ export const MyCustomExtension = ({ onChange, rawErrors, required, formData }: F */ export const myCustomValidation = ( - value: string, - validation: FieldValidation, + value: string, + validation: FieldValidation, ) => { - if (!KubernetesValidatorFunctions.isValidObjectName(value)) { - validation.addError( - 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', - ); - } + if (!KubernetesValidatorFunctions.isValidObjectName(value)) { + validation.addError( + 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + ); + } }; ``` From c9e843547efbf73320f01aa7c3c3115dc30d4322 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Wed, 27 Oct 2021 14:03:28 -0400 Subject: [PATCH 191/245] Create `--path` directory if it does not already exist Instead of failing when the specified path does not exist, create it using `mkdirs`. This ensures that that either the directory already exists, or it's created. Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d833f0c4d8..270ed3fd54 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -27,7 +27,7 @@ import { Task, templatingTask } from './lib/tasks'; const exec = promisify(execCb); -async function checkAppPathDoesntExist(rootDir: string, name: string) { +async function checkAppExists(rootDir: string, name: string) { await Task.forItem('checking', name, async () => { const destination = resolvePath(rootDir, name); @@ -40,18 +40,13 @@ async function checkAppPathDoesntExist(rootDir: string, name: string) { }); } -async function checkPathExistsAndIsDirectory(path: string) { +async function checkPathExists(path: string) { await Task.forItem('checking', path, async () => { - if (await fs.pathExists(path)) { - if (!fs.lstatSync(path).isDirectory()) { - throw new Error( - 'Directory specified with --path argument is a file\nPlease ensure target path is a directory', - ); - } - } else { - throw new Error( - 'Directory specified with --path argument does not exist\nPlease try again with a different path', - ); + try { + await fs.mkdirs(path); + } catch (error) { + // will fail if a file already exists at given `path` + throw new Error(`Failed to create app directory: ${error.message}`); } }); } @@ -150,7 +145,7 @@ export default async (cmd: Command): Promise => { // Template directly to specified path Task.section('Checking that supplied path exists'); - await checkPathExistsAndIsDirectory(appDir); + await checkPathExists(appDir); Task.section('Preparing files'); await templatingTask(templateDir, cmd.path, answers); @@ -158,7 +153,7 @@ export default async (cmd: Command): Promise => { // Template to temporary location, and then move files Task.section('Checking if the directory is available'); - await checkAppPathDoesntExist(paths.targetDir, answers.name); + await checkAppExists(paths.targetDir, answers.name); Task.section('Creating a temporary app directory'); await createTemporaryAppFolder(tempDir); From b9ce1ce2c17bb66eac24738617ccf75cba5b5367 Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Wed, 27 Oct 2021 15:00:40 -0400 Subject: [PATCH 192/245] feat: allow custom location analyzer in next catalog builder Signed-off-by: Andrew Thauer --- .changeset/brave-icons-shop.md | 5 +++++ plugins/catalog-backend/api-report.md | 1 + .../src/service/NextCatalogBuilder.ts | 12 +++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-icons-shop.md diff --git a/.changeset/brave-icons-shop.md b/.changeset/brave-icons-shop.md new file mode 100644 index 0000000000..d2d48e8f80 --- /dev/null +++ b/.changeset/brave-icons-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Allow custom LocationAnalyzer in NextCatalogBuilder diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index cbef4c90a1..6d21884c12 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -1295,6 +1295,7 @@ export class NextCatalogBuilder { setEntityDataParser(parser: CatalogProcessorParser): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen setFieldFormatValidators(validators: Partial): NextCatalogBuilder; + setLocationAnalyzer(locationAnalyzer: LocationAnalyzer): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen setPlaceholderResolver( diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 60a1668f37..835c1dc700 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -122,6 +122,7 @@ export class NextCatalogBuilder { minSeconds: 100, maxSeconds: 150, }); + private locationAnalyzer: LocationAnalyzer | undefined = undefined; constructor(env: CatalogEnvironment) { this.env = env; @@ -176,6 +177,14 @@ export class NextCatalogBuilder { return this; } + /** + * Overwrites the default location analyzer. + */ + setLocationAnalyzer(locationAnalyzer: LocationAnalyzer): NextCatalogBuilder { + this.locationAnalyzer = locationAnalyzer; + return this; + } + /** * Sets what policies to use for validation of entities between the pre- * processing and post-processing stages. All such policies must pass for the @@ -338,7 +347,8 @@ export class NextCatalogBuilder { ); const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationAnalyzer = new RepoLocationAnalyzer(logger, integrations); + const locationAnalyzer = + this.locationAnalyzer ?? new RepoLocationAnalyzer(logger, integrations); const locationService = new DefaultLocationService( locationStore, orchestrator, From 328f77f76bf625085269a7e63e1131ec6712e2a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Oct 2021 04:11:31 +0000 Subject: [PATCH 193/245] build(deps): bump @rjsf/core from 3.1.0 to 3.2.0 Bumps [@rjsf/core](https://github.com/rjsf-team/react-jsonschema-form) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/rjsf-team/react-jsonschema-form/releases) - [Changelog](https://github.com/rjsf-team/react-jsonschema-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/rjsf-team/react-jsonschema-form/compare/v3.1.0...v3.2.0) --- updated-dependencies: - dependency-name: "@rjsf/core" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index b957819540..befa80ef5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5141,9 +5141,9 @@ react-lifecycles-compat "^3.0.4" "@rjsf/core@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@rjsf/core/-/core-3.1.0.tgz#7667364a4532e25c164abbc2adabe39157ffec4c" - integrity sha512-EwM2juiQxEdXzFy9rIIsDr6/e+FYeR1cKx0/no8ASEuXZ1p+/nf/CxKGobzD6UhpRip7JK9PhhuHFEFBIjHFJA== + version "3.2.0" + resolved "https://registry.npmjs.org/@rjsf/core/-/core-3.2.0.tgz#189ff7fb132cb223463792d75fb1e04e9dd5d9f9" + integrity sha512-Cgonq9bBWlpE0yeeIv7rAIDuJvO1wAvp9G7tT/UvhODAyb4tDb1q8J/8ubWuRRiEyzYUkuxN0W8uPIzQsJgtTQ== dependencies: "@types/json-schema" "^7.0.7" ajv "^6.7.0" @@ -11459,21 +11459,11 @@ core-js-compat@^3.9.0, core-js-compat@^3.9.1: browserslist "^4.16.6" semver "7.0.0" -core-js-pure@^3.10.2, core-js-pure@^3.6.5: - version "3.15.1" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.1.tgz#8356f6576fa2aa8e54ca6fe02620968ff010eed7" - integrity sha512-OZuWHDlYcIda8sJLY4Ec6nWq2hRjlyCqCZ+jCflyleMkVt3tPedDVErvHslyS2nbO+SlBFMSBJYvtLMwxnrzjA== - -core-js-pure@^3.16.0: +core-js-pure@^3.10.2, core-js-pure@^3.16.0, core-js-pure@^3.6.5, core-js-pure@^3.8.2: version "3.16.2" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8" integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw== -core-js-pure@^3.8.2: - version "3.16.0" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.0.tgz#218e07add3f1844e53fab195c47871fc5ba18de8" - integrity sha512-wzlhZNepF/QA9yvx3ePDgNGudU5KDB8lu/TRPKelYA/QtSnkS/cLl2W+TIdEX1FAFcBr0YpY7tPDlcmXJ7AyiQ== - core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: version "2.6.12" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" From 203665b99b29f5fc1b56a9982bd5824b8e887583 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Oct 2021 04:12:29 +0000 Subject: [PATCH 194/245] build(deps): bump webpack from 5.48.0 to 5.60.0 Bumps [webpack](https://github.com/webpack/webpack) from 5.48.0 to 5.60.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.48.0...v5.60.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 56 ++++++++++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/yarn.lock b/yarn.lock index b957819540..12b016ddfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6842,21 +6842,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "0.0.44" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21" - integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g== +"@types/estree@*", "@types/estree@^0.0.50": + version "0.0.50" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.50": - version "0.0.50" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== - "@types/expect@^1.20.4": version "1.20.4" resolved "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" @@ -13184,10 +13179,10 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enhanced-resolve@^5.8.0: - version "5.8.2" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" - integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== +enhanced-resolve@^5.8.3: + version "5.8.3" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" + integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -13300,10 +13295,10 @@ es-get-iterator@^1.0.2: is-string "^1.0.5" isarray "^2.0.5" -es-module-lexer@^0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d" - integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw== +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== es-to-primitive@^1.2.1: version "1.2.1" @@ -20125,24 +20120,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.48.0, "mime-db@>= 1.43.0 < 2": - version "1.48.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" - integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== - mime-db@1.49.0: version "1.49.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== -mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.31" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" - integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== - dependencies: - mime-db "1.48.0" +"mime-db@>= 1.43.0 < 2": + version "1.48.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== -mime-types@^2.1.31: +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.32" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== @@ -28478,9 +28466,9 @@ webpack@4: webpack-sources "^1.4.1" webpack@^5, webpack@^5.48.0: - version "5.48.0" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.48.0.tgz#06180fef9767a6fd066889559a4c4d49bee19b83" - integrity sha512-CGe+nfbHrYzbk7SKoYITCgN3LRAG0yVddjNUecz9uugo1QtYdiyrVD8nP1PhkNqPfdxC2hknmmKpP355Epyn6A== + version "5.60.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.60.0.tgz#9c26f38a57c9688b0a8c5c885e05197344eae67d" + integrity sha512-OL5GDYi2dKxnwJPSOg2tODgzDxAffN0osgWkZaBo/l3ikCxDFP+tuJT3uF7GyBE3SDBpKML/+a8EobyWAQO3DQ== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.50" @@ -28491,8 +28479,8 @@ webpack@^5, webpack@^5.48.0: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.0" - es-module-lexer "^0.7.1" + enhanced-resolve "^5.8.3" + es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" From 12aac2d0f5c40b107656768fd7ce0acbcca233f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Oct 2021 04:15:13 +0000 Subject: [PATCH 195/245] build(deps): bump core-js from 3.15.0 to 3.19.0 Bumps [core-js](https://github.com/zloirock/core-js) from 3.15.0 to 3.19.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.15.0...v3.19.0) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b957819540..5346fbf236 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11480,9 +11480,9 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2: - version "3.15.0" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044" - integrity sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw== + version "3.19.0" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.19.0.tgz#9e40098a9bc326c7e81b486abbd5e12b9d275176" + integrity sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" From 66b5024b7c1eb4a944d79271a30d007405941c08 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 28 Oct 2021 13:02:07 +0100 Subject: [PATCH 196/245] only create datastore if needed for the broker Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/src/service/router.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 231c998f8b..814b235bf2 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -89,12 +89,17 @@ export async function createRouter( const workingDirectory = await getWorkingDirectory(config, logger); const entityClient = new CatalogEntityClient(catalogClient); const integrations = ScmIntegrations.fromConfig(config); + let taskBroker: TaskBroker; + + if (!options.taskBroker) { + const databaseTaskStore = await DatabaseTaskStore.create({ + database: await database.getClient(), + }); + taskBroker = new StorageTaskBroker(databaseTaskStore, logger); + } else { + taskBroker = options.taskBroker; + } - const databaseTaskStore = await DatabaseTaskStore.create({ - database: await database.getClient(), - }); - const taskBroker = - options.taskBroker || new StorageTaskBroker(databaseTaskStore, logger); const actionRegistry = new TemplateActionRegistry(); const workers = []; From 91f8644f17651c5e780aff0f5f33b7c4d8d67e5d Mon Sep 17 00:00:00 2001 From: Xavier Serrano Date: Wed, 27 Oct 2021 19:15:13 +0200 Subject: [PATCH 197/245] Add auth token to KafkaBackendClient via IdentityApi Signed-off-by: Xavier Serrano Signed-off-by: Xavier Serrano --- .changeset/long-bugs-kiss.md | 5 +++++ plugins/kafka/src/api/KafkaBackendClient.ts | 11 +++++++++-- plugins/kafka/src/plugin.ts | 6 ++++-- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .changeset/long-bugs-kiss.md diff --git a/.changeset/long-bugs-kiss.md b/.changeset/long-bugs-kiss.md new file mode 100644 index 0000000000..49e0edae72 --- /dev/null +++ b/.changeset/long-bugs-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka': patch +--- + +Use IdentityApi to provide Auth Token for KafkaBackendClient Api calls diff --git a/plugins/kafka/src/api/KafkaBackendClient.ts b/plugins/kafka/src/api/KafkaBackendClient.ts index 988939ff1a..aea5736aad 100644 --- a/plugins/kafka/src/api/KafkaBackendClient.ts +++ b/plugins/kafka/src/api/KafkaBackendClient.ts @@ -15,21 +15,28 @@ */ import { KafkaApi, ConsumerGroupOffsetsResponse } from './types'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; export class KafkaBackendClient implements KafkaApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; } private async internalGet(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('kafka')}${path}`; + const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), }, }); diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index e389e5e27a..539b0a0fda 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -21,6 +21,7 @@ import { createRoutableExtension, createRouteRef, discoveryApiRef, + identityApiRef, } from '@backstage/core-plugin-api'; export const rootCatalogKafkaRouteRef = createRouteRef({ @@ -33,8 +34,9 @@ export const kafkaPlugin = createPlugin({ apis: [ createApiFactory({ api: kafkaApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new KafkaBackendClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new KafkaBackendClient({ discoveryApi, identityApi }), }), ], routes: { From 419054c97a98bcb93097c8c5585d483f69e7542e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 28 Oct 2021 10:50:35 +0200 Subject: [PATCH 198/245] Remove usage of deprecated Keyboard class Signed-off-by: Johan Haals --- .../HeaderActionMenu/HeaderActionMenu.test.tsx | 18 ++++++------------ packages/test-utils/src/testUtils/Keyboard.js | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx b/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx index b1bd398d15..58785c5c90 100644 --- a/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx +++ b/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx @@ -15,13 +15,10 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import { - wrapInTestApp, - Keyboard, - renderInTestApp, -} from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; import { HeaderActionMenu } from './HeaderActionMenu'; +import userEvent from '@testing-library/user-event'; describe('', () => { it('renders without any items and without exploding', async () => { @@ -89,16 +86,13 @@ describe('', () => { }); it('should close when hitting escape', async () => { - const rendered = render( - wrapInTestApp( - , - ), + const rendered = await renderInTestApp( + , ); - expect(rendered.container.getAttribute('aria-hidden')).toBeNull(); fireEvent.click(rendered.getByTestId('header-action-menu')); expect(rendered.container.getAttribute('aria-hidden')).toBe('true'); - await Keyboard.type(rendered, ''); + userEvent.type(rendered.getByTestId('header-action-menu'), '{esc}'); expect(rendered.container.getAttribute('aria-hidden')).toBeNull(); }); }); diff --git a/packages/test-utils/src/testUtils/Keyboard.js b/packages/test-utils/src/testUtils/Keyboard.js index 3f4724d562..0d46d26363 100644 --- a/packages/test-utils/src/testUtils/Keyboard.js +++ b/packages/test-utils/src/testUtils/Keyboard.js @@ -25,7 +25,7 @@ const codes = { /** * @public - * @deprecated because it has no usages. Perhaps resurfaced in the future when need be. + * @deprecated superseded by @testing-library/user-event */ export class Keyboard { static async type(target, input) { From f808392183bbe32adf2c61fa4dba8ba85c4e8f8a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 28 Oct 2021 13:38:41 +0200 Subject: [PATCH 199/245] reference library function Signed-off-by: Johan Haals --- packages/test-utils/src/testUtils/Keyboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-utils/src/testUtils/Keyboard.js b/packages/test-utils/src/testUtils/Keyboard.js index 0d46d26363..2a7928f8cd 100644 --- a/packages/test-utils/src/testUtils/Keyboard.js +++ b/packages/test-utils/src/testUtils/Keyboard.js @@ -25,7 +25,7 @@ const codes = { /** * @public - * @deprecated superseded by @testing-library/user-event + * @deprecated superseded by {@link @testing-library/user-event#userEvent} */ export class Keyboard { static async type(target, input) { From 71fd5cd735fe79a3182ae9de4a8b6f2f33918804 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 28 Oct 2021 14:58:08 +0200 Subject: [PATCH 200/245] add changeset Signed-off-by: Johan Haals --- .changeset/loud-bats-flow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loud-bats-flow.md diff --git a/.changeset/loud-bats-flow.md b/.changeset/loud-bats-flow.md new file mode 100644 index 0000000000..af4837dbbe --- /dev/null +++ b/.changeset/loud-bats-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Update Keyboard deprecation with a link to the recommended successor From 26f758ded514b2285c3c45384af8b435abe923af Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 16:00:15 +0200 Subject: [PATCH 201/245] chore: notice for down time Signed-off-by: blam --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1078631926..13b7c945cf 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # [Backstage](https://backstage.io) +> 🏖 All of the maintainers will be taking a wellness break Nov. 1–5. The repo and Discord may be quieter than usual, but not to worry. We’ll have coverage plans in place and be back in full force, rested and restored, on Nov. 8. 🏖 + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects) [![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22) From d66c5f12826b0499c914d276710b40a121a89aca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Oct 2021 14:06:43 +0000 Subject: [PATCH 202/245] Version Packages --- .changeset/brave-icons-shop.md | 5 ---- .changeset/bright-plants-travel.md | 5 ---- .changeset/cold-wolves-eat.md | 5 ---- .changeset/dirty-bugs-care.md | 9 ------ .changeset/dull-news-visit.md | 5 ---- .changeset/famous-foxes-invite.md | 5 ---- .changeset/fluffy-camels-watch.md | 5 ---- .changeset/forty-cooks-film.md | 5 ---- .changeset/friendly-olives-drop.md | 5 ---- .changeset/great-tomatoes-cheat.md | 5 ---- .changeset/green-jeans-beg.md | 5 ---- .changeset/green-parrots-thank.md | 10 ------- .changeset/green-tips-eat.md | 5 ---- .changeset/kind-islands-attack.md | 5 ---- .changeset/lazy-dodos-drum.md | 7 ----- .changeset/lucky-books-worry.md | 5 ---- .changeset/modern-elephants-applaud.md | 5 ---- .changeset/moody-years-prove.md | 6 ---- .changeset/neat-rings-protect.md | 5 ---- .changeset/odd-mirrors-drum.md | 5 ---- .changeset/olive-clouds-change.md | 5 ---- .changeset/perfect-ducks-promise.md | 5 ---- .changeset/popular-apples-sparkle.md | 26 ----------------- .changeset/quick-walls-know.md | 5 ---- .changeset/rotten-melons-carry.md | 8 ------ .changeset/rude-starfishes-walk.md | 7 ----- .changeset/search-spicy-foxes-joke.md | 5 ---- .changeset/slimy-frogs-allow.md | 5 ---- .changeset/techdocs-poor-paws-rest.md | 6 ---- .changeset/tough-avocados-give.md | 5 ---- .changeset/twenty-cups-suffer.md | 5 ---- .changeset/two-cougars-breathe.md | 5 ---- .changeset/unlucky-hotels-roll.md | 5 ---- .changeset/weak-readers-know.md | 5 ---- .changeset/wet-socks-grow.md | 5 ---- .changeset/wise-lamps-appear.md | 5 ---- packages/backend-common/CHANGELOG.md | 14 ++++++++++ packages/backend-common/package.json | 16 +++++------ packages/catalog-model/CHANGELOG.md | 9 ++++++ packages/catalog-model/package.json | 8 +++--- packages/cli-common/CHANGELOG.md | 6 ++++ packages/cli-common/package.json | 2 +- packages/cli/CHANGELOG.md | 14 ++++++++++ packages/cli/package.json | 24 ++++++++-------- packages/codemods/CHANGELOG.md | 10 +++++++ packages/codemods/package.json | 4 +-- packages/config-loader/CHANGELOG.md | 11 ++++++++ packages/config-loader/package.json | 8 +++--- packages/config/CHANGELOG.md | 8 ++++++ packages/config/package.json | 4 +-- packages/core-app-api/CHANGELOG.md | 14 ++++++++++ packages/core-app-api/package.json | 14 +++++----- packages/core-components/CHANGELOG.md | 12 ++++++++ packages/core-components/package.json | 16 +++++------ packages/core-plugin-api/CHANGELOG.md | 10 +++++++ packages/core-plugin-api/package.json | 12 ++++---- packages/create-app/CHANGELOG.md | 8 ++++++ packages/create-app/package.json | 4 +-- packages/errors/CHANGELOG.md | 8 ++++++ packages/errors/package.json | 4 +-- packages/integration-react/CHANGELOG.md | 12 ++++++++ packages/integration-react/package.json | 16 +++++------ packages/integration/CHANGELOG.md | 8 ++++++ packages/integration/package.json | 10 +++---- packages/search-common/CHANGELOG.md | 6 ++++ packages/search-common/package.json | 4 +-- packages/techdocs-common/CHANGELOG.md | 13 +++++++++ packages/techdocs-common/package.json | 16 +++++------ packages/test-utils-core/CHANGELOG.md | 9 ++++++ packages/test-utils-core/package.json | 2 +- packages/test-utils/CHANGELOG.md | 16 +++++++++++ packages/test-utils/package.json | 10 +++---- packages/theme/CHANGELOG.md | 7 +++++ packages/theme/package.json | 4 +-- plugins/allure/package.json | 12 ++++---- plugins/analytics-module-ga/package.json | 12 ++++---- plugins/api-docs/package.json | 16 +++++------ plugins/app-backend/CHANGELOG.md | 10 +++++++ plugins/app-backend/package.json | 10 +++---- plugins/auth-backend/CHANGELOG.md | 12 ++++++++ plugins/auth-backend/package.json | 14 +++++----- plugins/azure-devops-backend/CHANGELOG.md | 9 ++++++ plugins/azure-devops-backend/package.json | 8 +++--- plugins/azure-devops/CHANGELOG.md | 19 +++++++++++++ plugins/azure-devops/package.json | 20 ++++++------- plugins/badges/package.json | 12 ++++---- plugins/bazaar/package.json | 6 ++-- plugins/bitrise/package.json | 12 ++++---- .../catalog-backend-module-ldap/CHANGELOG.md | 11 ++++++++ .../catalog-backend-module-ldap/package.json | 12 ++++---- plugins/catalog-backend/CHANGELOG.md | 14 ++++++++++ plugins/catalog-backend/package.json | 18 ++++++------ plugins/catalog-graph/package.json | 24 ++++++++-------- plugins/catalog-graphql/CHANGELOG.md | 9 ++++++ plugins/catalog-graphql/package.json | 10 +++---- plugins/catalog-import/package.json | 14 +++++----- plugins/catalog-react/CHANGELOG.md | 14 ++++++++++ plugins/catalog-react/package.json | 18 ++++++------ plugins/catalog/package.json | 14 +++++----- plugins/circleci/package.json | 12 ++++---- plugins/cloudbuild/package.json | 12 ++++---- plugins/code-coverage/CHANGELOG.md | 14 ++++++++++ plugins/code-coverage/package.json | 22 +++++++-------- plugins/config-schema/CHANGELOG.md | 12 ++++++++ plugins/config-schema/package.json | 18 ++++++------ plugins/cost-insights/package.json | 12 ++++---- plugins/explore/package.json | 12 ++++---- plugins/firehydrant/package.json | 12 ++++---- plugins/fossa/package.json | 12 ++++---- plugins/gcp-projects/package.json | 12 ++++---- plugins/git-release-manager/package.json | 12 ++++---- plugins/github-actions/package.json | 12 ++++---- plugins/github-deployments/package.json | 12 ++++---- plugins/gitops-profiles/package.json | 12 ++++---- plugins/graphiql/package.json | 12 ++++---- plugins/home/package.json | 12 ++++---- plugins/ilert/package.json | 12 ++++---- plugins/jenkins/package.json | 12 ++++---- plugins/kafka/package.json | 12 ++++---- plugins/kubernetes/CHANGELOG.md | 13 +++++++++ plugins/kubernetes/package.json | 20 ++++++------- plugins/lighthouse/package.json | 12 ++++---- plugins/newrelic/package.json | 12 ++++---- plugins/org/package.json | 16 +++++------ plugins/pagerduty/package.json | 12 ++++---- plugins/rollbar/package.json | 12 ++++---- .../CHANGELOG.md | 12 ++++++++ .../package.json | 14 +++++----- .../CHANGELOG.md | 12 ++++++++ .../package.json | 14 +++++----- .../CHANGELOG.md | 10 +++++++ .../package.json | 8 +++--- plugins/scaffolder-backend/CHANGELOG.md | 17 +++++++++++ plugins/scaffolder-backend/package.json | 22 +++++++-------- plugins/scaffolder-common/CHANGELOG.md | 9 ++++++ plugins/scaffolder-common/package.json | 6 ++-- plugins/scaffolder/CHANGELOG.md | 18 ++++++++++++ plugins/scaffolder/package.json | 26 ++++++++--------- plugins/search-backend-node/CHANGELOG.md | 8 ++++++ plugins/search-backend-node/package.json | 8 +++--- plugins/search/CHANGELOG.md | 15 ++++++++++ plugins/search/package.json | 24 ++++++++-------- plugins/sentry/CHANGELOG.md | 13 +++++++++ plugins/sentry/package.json | 18 ++++++------ plugins/shortcuts/CHANGELOG.md | 10 +++++++ plugins/shortcuts/package.json | 14 +++++----- plugins/sonarqube/package.json | 12 ++++---- plugins/splunk-on-call/package.json | 12 ++++---- plugins/tech-radar/package.json | 12 ++++---- plugins/techdocs-backend/CHANGELOG.md | 14 ++++++++++ plugins/techdocs-backend/package.json | 20 ++++++------- plugins/techdocs/CHANGELOG.md | 18 ++++++++++++ plugins/techdocs/package.json | 28 +++++++++---------- plugins/todo/package.json | 12 ++++---- plugins/user-settings/package.json | 12 ++++---- plugins/xcmetrics/package.json | 12 ++++---- 156 files changed, 988 insertions(+), 719 deletions(-) delete mode 100644 .changeset/brave-icons-shop.md delete mode 100644 .changeset/bright-plants-travel.md delete mode 100644 .changeset/cold-wolves-eat.md delete mode 100644 .changeset/dirty-bugs-care.md delete mode 100644 .changeset/dull-news-visit.md delete mode 100644 .changeset/famous-foxes-invite.md delete mode 100644 .changeset/fluffy-camels-watch.md delete mode 100644 .changeset/forty-cooks-film.md delete mode 100644 .changeset/friendly-olives-drop.md delete mode 100644 .changeset/great-tomatoes-cheat.md delete mode 100644 .changeset/green-jeans-beg.md delete mode 100644 .changeset/green-parrots-thank.md delete mode 100644 .changeset/green-tips-eat.md delete mode 100644 .changeset/kind-islands-attack.md delete mode 100644 .changeset/lazy-dodos-drum.md delete mode 100644 .changeset/lucky-books-worry.md delete mode 100644 .changeset/modern-elephants-applaud.md delete mode 100644 .changeset/moody-years-prove.md delete mode 100644 .changeset/neat-rings-protect.md delete mode 100644 .changeset/odd-mirrors-drum.md delete mode 100644 .changeset/olive-clouds-change.md delete mode 100644 .changeset/perfect-ducks-promise.md delete mode 100644 .changeset/popular-apples-sparkle.md delete mode 100644 .changeset/quick-walls-know.md delete mode 100644 .changeset/rotten-melons-carry.md delete mode 100644 .changeset/rude-starfishes-walk.md delete mode 100644 .changeset/search-spicy-foxes-joke.md delete mode 100644 .changeset/slimy-frogs-allow.md delete mode 100644 .changeset/techdocs-poor-paws-rest.md delete mode 100644 .changeset/tough-avocados-give.md delete mode 100644 .changeset/twenty-cups-suffer.md delete mode 100644 .changeset/two-cougars-breathe.md delete mode 100644 .changeset/unlucky-hotels-roll.md delete mode 100644 .changeset/weak-readers-know.md delete mode 100644 .changeset/wet-socks-grow.md delete mode 100644 .changeset/wise-lamps-appear.md create mode 100644 plugins/scaffolder-backend-module-yeoman/CHANGELOG.md create mode 100644 plugins/scaffolder-common/CHANGELOG.md diff --git a/.changeset/brave-icons-shop.md b/.changeset/brave-icons-shop.md deleted file mode 100644 index d2d48e8f80..0000000000 --- a/.changeset/brave-icons-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Allow custom LocationAnalyzer in NextCatalogBuilder diff --git a/.changeset/bright-plants-travel.md b/.changeset/bright-plants-travel.md deleted file mode 100644 index ad8a77d17b..0000000000 --- a/.changeset/bright-plants-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser. diff --git a/.changeset/cold-wolves-eat.md b/.changeset/cold-wolves-eat.md deleted file mode 100644 index fa3a2180b2..0000000000 --- a/.changeset/cold-wolves-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -feature: sentry-plugin allows passing search query for listing issues diff --git a/.changeset/dirty-bugs-care.md b/.changeset/dirty-bugs-care.md deleted file mode 100644 index 4d90a9c939..0000000000 --- a/.changeset/dirty-bugs-care.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/test-utils': patch -'@backstage/test-utils-core': patch ---- - -Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. -This should have no impact since this package is considered internal and have no usages outside core packages. - -Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. diff --git a/.changeset/dull-news-visit.md b/.changeset/dull-news-visit.md deleted file mode 100644 index e1fab41ebf..0000000000 --- a/.changeset/dull-news-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Reader will now scroll to the top of the page when navigating between pages diff --git a/.changeset/famous-foxes-invite.md b/.changeset/famous-foxes-invite.md deleted file mode 100644 index 5cb9f70e75..0000000000 --- a/.changeset/famous-foxes-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Update usage of msw in default plugin template diff --git a/.changeset/fluffy-camels-watch.md b/.changeset/fluffy-camels-watch.md deleted file mode 100644 index ddbf218f97..0000000000 --- a/.changeset/fluffy-camels-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config': patch ---- - -Minor exports cleanup diff --git a/.changeset/forty-cooks-film.md b/.changeset/forty-cooks-film.md deleted file mode 100644 index 20ff746949..0000000000 --- a/.changeset/forty-cooks-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/theme': patch ---- - -More theme API cleanup diff --git a/.changeset/friendly-olives-drop.md b/.changeset/friendly-olives-drop.md deleted file mode 100644 index 371cf0e4db..0000000000 --- a/.changeset/friendly-olives-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page diff --git a/.changeset/great-tomatoes-cheat.md b/.changeset/great-tomatoes-cheat.md deleted file mode 100644 index 547e89c6d0..0000000000 --- a/.changeset/great-tomatoes-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-common': patch ---- - -Add docs to the last export of cli-common diff --git a/.changeset/green-jeans-beg.md b/.changeset/green-jeans-beg.md deleted file mode 100644 index bafc33fad9..0000000000 --- a/.changeset/green-jeans-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -bump `@spotify/eslint-config-base` from 9.0.2 to 12.0.0 diff --git a/.changeset/green-parrots-thank.md b/.changeset/green-parrots-thank.md deleted file mode 100644 index 9296a5119a..0000000000 --- a/.changeset/green-parrots-thank.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch ---- - -Azure DevOps frontend refactoring items from issue #7641 - -- Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions -- Improved documentation to be easier to expand with new features in the future -- Removed Router based on feedback from maintainers -- Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable diff --git a/.changeset/green-tips-eat.md b/.changeset/green-tips-eat.md deleted file mode 100644 index b8c631fbf4..0000000000 --- a/.changeset/green-tips-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Scaffolder: Enable back Template action buttons if template fails to execute diff --git a/.changeset/kind-islands-attack.md b/.changeset/kind-islands-attack.md deleted file mode 100644 index a549839bc7..0000000000 --- a/.changeset/kind-islands-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-azure-devops-backend': patch ---- - -Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641 diff --git a/.changeset/lazy-dodos-drum.md b/.changeset/lazy-dodos-drum.md deleted file mode 100644 index b4d72aca28..0000000000 --- a/.changeset/lazy-dodos-drum.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/errors': patch -'@backstage/integration': patch -'@backstage/theme': patch ---- - -More API fixes: mark things public, add docs, fix exports diff --git a/.changeset/lucky-books-worry.md b/.changeset/lucky-books-worry.md deleted file mode 100644 index 5645b4b4e9..0000000000 --- a/.changeset/lucky-books-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Allow OAuth state to be encoded by a stateEncoder. diff --git a/.changeset/modern-elephants-applaud.md b/.changeset/modern-elephants-applaud.md deleted file mode 100644 index 0f71bd7313..0000000000 --- a/.changeset/modern-elephants-applaud.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Adjusted some API exports diff --git a/.changeset/moody-years-prove.md b/.changeset/moody-years-prove.md deleted file mode 100644 index 41c45d9cc9..0000000000 --- a/.changeset/moody-years-prove.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/test-utils': patch ---- - -Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). diff --git a/.changeset/neat-rings-protect.md b/.changeset/neat-rings-protect.md deleted file mode 100644 index 9242d4ce6c..0000000000 --- a/.changeset/neat-rings-protect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Support optional path argument when generating a project using `create-app` diff --git a/.changeset/odd-mirrors-drum.md b/.changeset/odd-mirrors-drum.md deleted file mode 100644 index 290ba7c526..0000000000 --- a/.changeset/odd-mirrors-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Updates the d3 dependencies diff --git a/.changeset/olive-clouds-change.md b/.changeset/olive-clouds-change.md deleted file mode 100644 index 7b367710ac..0000000000 --- a/.changeset/olive-clouds-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration-react': patch ---- - -Clean up the API exports diff --git a/.changeset/perfect-ducks-promise.md b/.changeset/perfect-ducks-promise.md deleted file mode 100644 index 976e636400..0000000000 --- a/.changeset/perfect-ducks-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -EntityTypePicker can be hidden and have an initial filter value set, similar to EntityKindPicker diff --git a/.changeset/popular-apples-sparkle.md b/.changeset/popular-apples-sparkle.md deleted file mode 100644 index b8437795da..0000000000 --- a/.changeset/popular-apples-sparkle.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/catalog-model': patch -'@backstage/cli': patch -'@backstage/config-loader': patch -'@backstage/core-app-api': patch -'@backstage/errors': patch -'@backstage/search-common': patch -'@backstage/test-utils': patch -'@backstage/plugin-app-backend': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-catalog-graphql': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/plugin-scaffolder-backend-module-yeoman': patch -'@backstage/plugin-scaffolder-common': patch -'@backstage/plugin-search': patch -'@backstage/plugin-shortcuts': patch ---- - -Switch to use the json and observable types from `@backstage/types` diff --git a/.changeset/quick-walls-know.md b/.changeset/quick-walls-know.md deleted file mode 100644 index 14dd78364f..0000000000 --- a/.changeset/quick-walls-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Support optional bucketRootPath configuration parameter in S3 and GCS publishers diff --git a/.changeset/rotten-melons-carry.md b/.changeset/rotten-melons-carry.md deleted file mode 100644 index 2f1429c399..0000000000 --- a/.changeset/rotten-melons-carry.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/config': patch -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. diff --git a/.changeset/rude-starfishes-walk.md b/.changeset/rude-starfishes-walk.md deleted file mode 100644 index 87ec76fd0a..0000000000 --- a/.changeset/rude-starfishes-walk.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/config': patch -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch ---- - -Replace usage of test-utils-core with test-utils diff --git a/.changeset/search-spicy-foxes-joke.md b/.changeset/search-spicy-foxes-joke.md deleted file mode 100644 index c1ed318f6d..0000000000 --- a/.changeset/search-spicy-foxes-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': patch ---- - -Handle special case when filter array has single value optimizing Lunr search behaviour. diff --git a/.changeset/slimy-frogs-allow.md b/.changeset/slimy-frogs-allow.md deleted file mode 100644 index 41cc0119ef..0000000000 --- a/.changeset/slimy-frogs-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Any set configurations which have been tagged with a visibility 'secret', are now redacted from log lines. diff --git a/.changeset/techdocs-poor-paws-rest.md b/.changeset/techdocs-poor-paws-rest.md deleted file mode 100644 index 71e00f18ba..0000000000 --- a/.changeset/techdocs-poor-paws-rest.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. diff --git a/.changeset/tough-avocados-give.md b/.changeset/tough-avocados-give.md deleted file mode 100644 index 08b8a7ea72..0000000000 --- a/.changeset/tough-avocados-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -fix: sentry-plugin correction for allowed period values diff --git a/.changeset/twenty-cups-suffer.md b/.changeset/twenty-cups-suffer.md deleted file mode 100644 index 1dcb494ee8..0000000000 --- a/.changeset/twenty-cups-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -Api cleanup, adding `@public` where necessary and tweaking some comments diff --git a/.changeset/two-cougars-breathe.md b/.changeset/two-cougars-breathe.md deleted file mode 100644 index 0b61078e85..0000000000 --- a/.changeset/two-cougars-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fixed bug where the mode of an executable file was ignored diff --git a/.changeset/unlucky-hotels-roll.md b/.changeset/unlucky-hotels-roll.md deleted file mode 100644 index 0ba616c2c3..0000000000 --- a/.changeset/unlucky-hotels-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey` diff --git a/.changeset/weak-readers-know.md b/.changeset/weak-readers-know.md deleted file mode 100644 index 24cf29a5db..0000000000 --- a/.changeset/weak-readers-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-code-coverage': patch ---- - -Addresses bug when file path contains multiple directories with the same name. diff --git a/.changeset/wet-socks-grow.md b/.changeset/wet-socks-grow.md deleted file mode 100644 index 8ccbb7a2a2..0000000000 --- a/.changeset/wet-socks-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/errors': patch ---- - -Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` diff --git a/.changeset/wise-lamps-appear.md b/.changeset/wise-lamps-appear.md deleted file mode 100644 index 4189e55d49..0000000000 --- a/.changeset/wise-lamps-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -bump `typescript-json-schema` from 0.50.1 to 0.51.0 diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 131b160a36..f0368b55d4 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/backend-common +## 0.9.8 + +### Patch Changes + +- 96cfa561eb: Adjusted some API exports +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 1be8d2abdb: Any set configurations which have been tagged with a visibility 'secret', are now redacted from log lines. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/config-loader@0.7.1 + ## 0.9.7 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index bee663a317..a174862b4a 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.7", + "version": "0.9.8", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", - "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.7.0", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.7", + "@backstage/cli-common": "^0.1.5", + "@backstage/config": "^0.1.11", + "@backstage/config-loader": "^0.7.1", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", "@lerna/project": "^4.0.0", @@ -79,8 +79,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 066b01cd50..e0c067c9c7 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/catalog-model +## 0.9.6 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + ## 0.9.5 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 615e80b684..b64e536578 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.5", + "version": "0.9.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.13", @@ -42,7 +42,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index b4b87fa103..f3978798c6 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli-common +## 0.1.5 + +### Patch Changes + +- c2d50a0073: Add docs to the last export of cli-common + ## 0.1.4 ### Patch Changes diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 0693b555a6..5916cdb017 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.4", + "version": "0.1.5", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7cb3c5be91..48f38cbc8a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/cli +## 0.8.1 + +### Patch Changes + +- f1e96dc5b1: Update usage of msw in default plugin template +- b0dc1fd241: bump `@spotify/eslint-config-base` from 9.0.2 to 12.0.0 +- c5bb1df55d: Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + - @backstage/config-loader@0.7.1 + ## 0.8.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index fa42c8520b..c023313fe7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.8.0", + "version": "0.8.1", "private": false, "publishConfig": { "access": "public" @@ -28,10 +28,10 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", - "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.7.0", - "@backstage/errors": "^0.1.3", + "@backstage/cli-common": "^0.1.5", + "@backstage/config": "^0.1.11", + "@backstage/config-loader": "^0.7.1", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", @@ -117,14 +117,14 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/core-app-api": "^0.1.18", + "@backstage/backend-common": "^0.9.8", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", - "@backstage/theme": "^0.2.11", + "@backstage/test-utils": "^0.1.20", + "@backstage/theme": "^0.2.12", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 7aabaef583..fbb2076167 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/codemods +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.5 + - @backstage/core-components@0.7.2 + - @backstage/core-app-api@0.1.19 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.19 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 77bdd181be..0dc0e9a0d5 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.19", + "version": "0.1.20", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "backstage-codemods": "bin/backstage-codemods" }, "dependencies": { - "@backstage/cli-common": "0.1.4", + "@backstage/cli-common": "0.1.5", "@backstage/core-app-api": "*", "@backstage/core-components": "*", "@backstage/core-plugin-api": "*", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 0b4ef9a5f3..dcaec00816 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/config-loader +## 0.7.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- ea21f7f567: bump `typescript-json-schema` from 0.50.1 to 0.51.0 +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + ## 0.7.0 ### Minor Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 2d15c31438..604caa4008 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.7.0", + "version": "0.7.1", "private": false, "publishConfig": { "access": "public", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", - "@backstage/config": "^0.1.9", - "@backstage/errors": "^0.1.3", + "@backstage/cli-common": "^0.1.5", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 9735c330c1..de62de5466 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/config +## 0.1.11 + +### Patch Changes + +- 10d267a1b7: Minor exports cleanup +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils + ## 0.1.10 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index 719ab22772..d162b73cae 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.10", + "version": "0.1.11", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index e01e89f04c..ff75f5bf81 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-app-api +## 0.1.19 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils +- 6b615e92c8: Api cleanup, adding `@public` where necessary and tweaking some comments +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.18 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index cc79a6458e..f40e3d5eb7 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.18", + "version": "0.1.19", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/config": "^0.1.11", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -46,8 +46,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 375f23dfda..af03762094 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/core-components +## 0.7.2 + +### Patch Changes + +- afd5c82ce1: Updates the d3 dependencies +- c6c2942daa: Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-plugin-api@0.1.12 + ## 0.7.1 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 038b99ca90..7f50af7488 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.7.1", + "version": "0.7.2", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", + "@backstage/config": "^0.1.11", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/theme": "^0.2.12", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -67,9 +67,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.18", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/core-app-api": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 21ce590e1c..9d8246f931 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-plugin-api +## 0.1.12 + +### Patch Changes + +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + ## 0.1.11 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 842ca102e2..23bf0fc8b0 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.1.11", + "version": "0.1.12", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.9", - "@backstage/theme": "^0.2.9", + "@backstage/config": "^0.1.11", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -43,9 +43,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 070d558124..05e067ce2c 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.4.1 + +### Patch Changes + +- 4f1c30c176: Support optional path argument when generating a project using `create-app` +- Updated dependencies + - @backstage/cli-common@0.1.5 + ## 0.4.0 ### Minor Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 6cf606ed49..725f865435 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.0", + "version": "0.4.1", "private": false, "publishConfig": { "access": "public" @@ -27,7 +27,7 @@ "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", + "@backstage/cli-common": "^0.1.5", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "9.1.0", diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index 368fa26593..9ea2f47cbb 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/errors +## 0.1.4 + +### Patch Changes + +- a15d028517: More API fixes: mark things public, add docs, fix exports +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 8c30ae8902: Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` + ## 0.1.3 ### Patch Changes diff --git a/packages/errors/package.json b/packages/errors/package.json index b1f214e5af..fe62bb3bba 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/errors", "description": "Common utilities for error handling within Backstage", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index cddaf2e792..a2a0d0c368 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/integration-react +## 0.1.13 + +### Patch Changes + +- 36e2b548cb: Clean up the API exports +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.12 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 9ee94f6e9e..b703203ab2 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "0.1.12", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/integration": "^0.6.8", - "@backstage/theme": "^0.2.11", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/integration": "^0.6.9", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -34,9 +34,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 327b9e5fcc..b9c6601e6c 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration +## 0.6.9 + +### Patch Changes + +- a15d028517: More API fixes: mark things public, add docs, fix exports +- Updated dependencies + - @backstage/config@0.1.11 + ## 0.6.8 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index a5e48bbca4..bd49ecc419 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "0.6.8", + "version": "0.6.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", + "@backstage/config": "^0.1.11", "cross-fetch": "^3.0.6", "git-url-parse": "^11.6.0", "@octokit/rest": "^18.5.3", @@ -39,9 +39,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/config-loader": "^0.7.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/config-loader": "^0.7.1", + "@backstage/test-utils": "^0.1.20", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "msw": "^0.35.0" diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index 34f956586e..e9f45d522e 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/search-common +## 0.2.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` + ## 0.2.0 ### Minor Changes diff --git a/packages/search-common/package.json b/packages/search-common/package.json index fee0fe59dd..e913ef66ff 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -39,7 +39,7 @@ "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0" + "@backstage/cli": "^0.8.1" }, "jest": { "roots": [ diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index fa32a0355e..b540f74549 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/techdocs-common +## 0.10.5 + +### Patch Changes + +- d207f6ee9e: Support optional bucketRootPath configuration parameter in S3 and GCS publishers +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + ## 0.10.4 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index fc3a9376dc..152b9b04f1 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.10.4", + "version": "0.10.5", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,12 +38,12 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.7", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.3", - "@backstage/search-common": "^0.2.0", - "@backstage/integration": "^0.6.7", + "@backstage/backend-common": "^0.9.8", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/search-common": "^0.2.1", + "@backstage/integration": "^0.6.9", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.4", "@types/express": "^4.17.6", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/test-utils-core/CHANGELOG.md b/packages/test-utils-core/CHANGELOG.md index 24d84eb105..35bdf1b2c6 100644 --- a/packages/test-utils-core/CHANGELOG.md +++ b/packages/test-utils-core/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/test-utils-core +## 0.1.4 + +### Patch Changes + +- bb12aae352: Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. + This should have no impact since this package is considered internal and have no usages outside core packages. + + Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. + ## 0.1.3 ### Patch Changes diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 8256714560..ebe6decddb 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.3", + "version": "0.1.4", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index a47be922b7..a1fa9d3b6b 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/test-utils +## 0.1.20 + +### Patch Changes + +- bb12aae352: Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. + This should have no impact since this package is considered internal and have no usages outside core packages. + + Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. + +- c5bb1df55d: Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/core-app-api@0.1.19 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.19 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index a565131c83..269b7b2d38 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.19", + "version": "0.1.20", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.18", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-app-api": "^0.1.19", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.10.1", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 97d889f450..8709484c79 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/theme +## 0.2.12 + +### Patch Changes + +- 40cfec8b3f: More theme API cleanup +- a15d028517: More API fixes: mark things public, add docs, fix exports + ## 0.2.11 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 3bc209a782..1acc91de79 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.11", + "version": "0.2.12", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.8.0" + "@backstage/cli": "^0.8.1" }, "files": [ "dist" diff --git a/plugins/allure/package.json b/plugins/allure/package.json index fa1acbd0a5..63864a7239 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -23,10 +23,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 431ae2de52..25534e9577 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -22,9 +22,9 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -34,10 +34,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 38acd683e9..67045c2043 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -32,11 +32,11 @@ "dependencies": { "@asyncapi/react-component": "^0.23.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog": "^0.7.2", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,14 +49,14 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", - "swagger-ui-react": "^4.0.0-rc.3", - "swagger-client": "3.16.1" + "swagger-client": "3.16.1", + "swagger-ui-react": "^4.0.0-rc.3" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 1424936280..0658b10ebb 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-app-backend +## 0.3.18 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/backend-common@0.9.8 + - @backstage/config-loader@0.7.1 + ## 0.3.17 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index f73e6f0b87..d989457c79 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.17", + "version": "0.3.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config-loader": "^0.7.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.8", + "@backstage/config-loader": "^0.7.1", + "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index f8896c95ba..c1ba2038aa 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend +## 0.4.6 + +### Patch Changes + +- 3b767f19c9: Allow OAuth state to be encoded by a stateEncoder. +- Updated dependencies + - @backstage/test-utils@0.1.20 + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + ## 0.4.5 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d9dddc073b..5c9d70ea10 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.4.5", + "version": "0.4.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", + "@backstage/backend-common": "^0.9.8", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/test-utils": "^0.1.19", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/test-utils": "^0.1.20", "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", @@ -73,7 +73,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index a776aea332..b756e7ce4f 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-azure-devops-backend +## 0.1.4 + +### Patch Changes + +- 2eebc9bac3: Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641 +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/backend-common@0.9.8 + ## 0.1.3 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index c2625f7f68..8a468b0cc9 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config": "^0.1.9", + "@backstage/backend-common": "^0.9.8", + "@backstage/config": "^0.1.11", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "express": "^4.17.1", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.35.0" diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 3687c2ca6c..71904265dd 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-azure-devops +## 0.1.2 + +### Patch Changes + +- 7359623e87: Azure DevOps frontend refactoring items from issue #7641 + + - Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions + - Improved documentation to be easier to expand with new features in the future + - Removed Router based on feedback from maintainers + - Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable + +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.1 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index dcae436c9f..a071205711 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,12 +27,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 2bf77dd00a..0cc3a06509 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index a5dc9f7b1e..5a98abfc3a 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -23,8 +23,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.5", "@backstage/cli": "^0.8.0", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog": "^0.7.2", "@backstage/plugin-catalog-react": "^0.6.1", "@material-ui/core": "^4.12.2", @@ -39,7 +39,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@backstage/dev-utils": "^0.2.12", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index c5f36c2d08..c4403830f1 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 6bac00a7a3..a776f3f329 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.6 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.2 + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/catalog-model@0.9.6 + ## 0.3.5 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 51c8780a9d..a8c3b71345 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend modules that helps integrate towards LDAP", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-backend": "^0.17.2", "@backstage/types": "^0.1.1", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", @@ -40,7 +40,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index b098270431..a6bdc8407d 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend +## 0.17.2 + +### Patch Changes + +- b9ce1ce2c1: Allow custom LocationAnalyzer in NextCatalogBuilder +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + ## 0.17.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 623f991bb7..121dd933f9 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.17.1", + "version": "0.17.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,13 +30,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", + "@backstage/backend-common": "^0.9.8", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/search-common": "^0.2.0", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/search-common": "^0.2.1", "@backstage/types": "^0.1.1", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", @@ -63,8 +63,8 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.8", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index b0dca011ca..d5cacc12a6 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -23,32 +23,32 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@types/react": "*", + "classnames": "^2.3.1", + "lodash": "^4.17.15", + "p-limit": "^3.1.0", + "qs": "^6.9.4", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^17.2.4", - "classnames": "^2.3.1", "react-router": "6.0.0-beta.0", - "qs": "^6.9.4", - "lodash": "^4.17.15", - "p-limit": "^3.1.0" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", - "@backstage/core-app-api": "^0.1.18", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^13.1.8", "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 6a909c57b7..e123f3b17f 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-graphql +## 0.2.13 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/catalog-model@0.9.6 + ## 0.2.12 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index da0c106177..5df4f9ba5c 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.2.12", + "version": "0.2.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.8", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -43,8 +43,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.11", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/typescript": "^1.17.7", "@graphql-codegen/typescript-resolvers": "^1.17.7", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index f79a3f136e..0287be8e06 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", @@ -46,19 +46,19 @@ "@types/react": "*", "git-url-parse": "^11.6.0", "js-base64": "^3.6.0", + "lodash": "^4.17.21", "react": "^16.13.1", "react-dom": "^16.13.1", "react-hook-form": "^7.12.2", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", - "yaml": "^1.10.0", - "lodash": "^4.17.21" + "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 26999f9e35..cdb80ceac4 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-react +## 0.6.2 + +### Patch Changes + +- f9cc2509f8: EntityTypePicker can be hidden and have an initial filter value set, similar to EntityKindPicker +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-app-api@0.1.19 + - @backstage/core-plugin-api@0.1.12 + ## 0.6.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 404ba6893f..40c0d9c739 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.6.1", + "version": "0.6.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/core-app-api": "^0.1.18", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", + "@backstage/catalog-model": "^0.9.6", + "@backstage/core-app-api": "^0.1.19", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -51,8 +51,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0557a047e6..0bc7ac4034 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,12 +33,12 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.3", "@backstage/integration-react": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 8a72ccf9c9..9a7c428d33 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 481910954c..376dd9f091 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 4bf3c6ea49..8a8c91a111 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-code-coverage +## 0.1.16 + +### Patch Changes + +- 767fe912b2: Addresses bug when file path contains multiple directories with the same name. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.15 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index b3a87964d3..2b09b4d99f 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.1.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,13 +21,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -41,10 +41,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 9e6e82c0c3..c8372532f6 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-config-schema +## 0.1.12 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.11 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 7445af4efb..e460878691 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,10 +37,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 922be98631..010498a272 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -55,10 +55,10 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index ff2068b307..66bd3fdf7a 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -32,11 +32,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", "@backstage/plugin-explore-react": "^0.0.6", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index dc742b5043..0738980613 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index c8955dfff8..c5b6b8b62f 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -33,11 +33,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 4e91dbfdc6..65458ea88b 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 8944745679..11f6655c5d 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/integration": "^0.6.8", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 6208b459ec..087a596c64 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -34,11 +34,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/integration": "^0.6.8", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index c373c91e7d..a405800387 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -22,13 +22,13 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 6fbd0316fc..cdebce653f 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 8af52123ee..b21281ff80 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/home/package.json b/plugins/home/package.json index 950dc81af6..e100dba9ca 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 674b9b56ee..f33e34beed 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -22,11 +22,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@date-io/luxon": "2.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,10 +39,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index d339eafb03..1bd6388fba 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index de8994ee5f..e9fe6bc4bf 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index a0a4ef2a51..ac7b19a1d9 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-kubernetes +## 0.4.18 + +### Patch Changes + +- 14df942bae: The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.4.17 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 673781b582..5bce077278 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.4.17", + "version": "0.4.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/plugin-catalog-react": "^0.6.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/plugin-catalog-react": "^0.6.2", "@backstage/plugin-kubernetes-common": "^0.1.5", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@kubernetes/client-node": "^0.15.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 84b2e5b601..1f3dd91d32 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,10 +34,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index f9c4c818a5..c60f1ee9a5 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/package.json b/plugins/org/package.json index 9ee26eefef..dc91b59816 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,25 +22,25 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "qs": "^6.10.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4", - "qs": "^6.10.1" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 6b0cdaaa53..2efc824746 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 7c9531abeb..38e295a37b 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index bf7db656c5..9e870a9e0b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.1.3 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/plugin-scaffolder-backend@0.15.11 + ## 0.1.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index fc6a79f18b..38c2eb493c 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,11 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.2", - "@backstage/plugin-scaffolder-backend": "^0.15.10", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.8", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/plugin-scaffolder-backend": "^0.15.11", + "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "10.0.0", @@ -33,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 721160b06e..f1e53abc5b 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.1.6 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/plugin-scaffolder-backend@0.15.11 + ## 0.1.5 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 5e1c9d0107..e79b7fb818 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.1.5", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,17 +21,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/plugin-scaffolder-backend": "^0.15.10", - "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.2", + "@backstage/backend-common": "^0.9.8", + "@backstage/plugin-scaffolder-backend": "^0.15.11", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md new file mode 100644 index 0000000000..b15614b0ba --- /dev/null +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -0,0 +1,10 @@ +# @backstage/plugin-scaffolder-backend-module-yeoman + +## 0.1.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/plugin-scaffolder-backend@0.15.11 diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index a1f1635808..b853309c56 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,14 +20,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.8", - "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/config": "^0.1.11", + "@backstage/plugin-scaffolder-backend": "^0.15.11", "@backstage/types": "^0.1.1", "winston": "^3.2.1", "yeoman-environment": "^3.6.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.2", + "@backstage/backend-common": "^0.9.8", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index d027ec52e7..689cb51d24 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-scaffolder-backend +## 0.15.11 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- e55a5dea09: Fixed bug where the mode of an executable file was ignored +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.2 + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.3 + - @backstage/plugin-scaffolder-common@0.1.1 + ## 0.15.10 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c6e7eaf409..fe5d61d4d3 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.10", + "version": "0.15.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,15 +30,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", + "@backstage/backend-common": "^0.9.8", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-backend": "^0.17.1", - "@backstage/plugin-scaffolder-common": "^0.1.0", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/plugin-catalog-backend": "^0.17.2", + "@backstage/plugin-scaffolder-common": "^0.1.1", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.3", "@backstage/types": "^0.1.1", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", @@ -71,8 +71,8 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md new file mode 100644 index 0000000000..db139b2782 --- /dev/null +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/plugin-scaffolder-common + +## 0.1.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/catalog-model@0.9.6 diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 3c1c444a78..006d8ad7aa 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,10 +36,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", + "@backstage/catalog-model": "^0.9.6", "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0" + "@backstage/cli": "^0.8.1" } } diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 119ed36685..b9873b5357 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-scaffolder +## 0.11.9 + +### Patch Changes + +- 5e10974af6: Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page +- 5df2435892: Scaffolder: Enable back Template action buttons if template fails to execute +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/integration-react@0.1.13 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.11.8 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 0e10fdb64c..0b517671ba 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.11.8", + "version": "0.11.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,15 +32,15 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/integration-react": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -65,10 +65,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 95ed3ad2d4..d4f4381f83 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-node +## 0.4.3 + +### Patch Changes + +- a369f19e7e: Handle special case when filter array has single value optimizing Lunr search behaviour. +- Updated dependencies + - @backstage/search-common@0.2.1 + ## 0.4.2 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 6681d31819..4514d07756 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "0.4.2", + "version": "0.4.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,14 +20,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/search-common": "^0.2.0", + "@backstage/search-common": "^0.2.1", "winston": "^3.2.1", "lunr": "^2.3.9", "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/cli": "^0.8.0" + "@backstage/backend-common": "^0.9.8", + "@backstage/cli": "^0.8.1" }, "files": [ "dist" diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index f92a021e5c..66c4a61ead 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-search +## 0.4.16 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + - @backstage/core-plugin-api@0.1.12 + ## 0.4.15 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 9d82da57f5..60e8a8eb3c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.4.15", + "version": "0.4.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,14 +30,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/search-common": "^0.2.0", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/search-common": "^0.2.1", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index cfaab7754c..2f44314100 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-sentry +## 0.3.27 + +### Patch Changes + +- 97231f5581: feature: sentry-plugin allows passing search query for listing issues +- 61fe752dad: fix: sentry-plugin correction for allowed period values +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.3.26 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 382870d033..005bfb2b3a 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.26", + "version": "0.3.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index 8e454f53e3..41581cc5b4 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-shortcuts +## 0.1.13 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.12 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 90c5addcf1..8b6f57bc52 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.1.12", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 5404bb0d8b..30cdd83368 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -34,10 +34,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 412ef74709..632de7e413 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 591435c737..31b12a97a9 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -31,9 +31,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -45,10 +45,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 221a681203..103306f79b 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-techdocs-backend +## 0.10.6 + +### Patch Changes + +- 106a5dc3ad: Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + - @backstage/techdocs-common@0.10.5 + ## 0.10.5 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index cb52d54d58..3208b451fa 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.10.5", + "version": "0.10.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", + "@backstage/backend-common": "^0.9.8", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.7", - "@backstage/search-common": "^0.2.0", - "@backstage/techdocs-common": "^0.10.4", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/search-common": "^0.2.1", + "@backstage/techdocs-common": "^0.10.5", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", @@ -51,8 +51,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@types/dockerode": "^3.2.1", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index f3b94c8579..a825b9a008 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-techdocs +## 0.12.4 + +### Patch Changes + +- a9a8c6f7c5: Reader will now scroll to the top of the page when navigating between pages +- 106a5dc3ad: Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/integration-react@0.1.13 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/plugin-search@0.4.16 + - @backstage/core-plugin-api@0.1.12 + ## 0.12.3 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 69ecce1aa9..c72bcab499 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.12.3", + "version": "0.12.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,17 +32,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/integration-react": "^0.1.12", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/integration-react": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/plugin-search": "^0.4.15", - "@backstage/theme": "^0.2.11", + "@backstage/plugin-catalog-react": "^0.6.2", + "@backstage/plugin-search": "^0.4.16", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -61,10 +61,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 30053ef1a5..9f27679615 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -41,10 +41,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index d6e665d99a..9a3b65f7ae 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,11 +43,11 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/core-plugin-api": "^0.1.11", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 7ece64c095..1ef31ecd6d 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/core-app-api": "^0.1.19", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", From 8796c00de828f81c478f2fcf0a66b6530a00804a Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 28 Oct 2021 15:09:51 +0100 Subject: [PATCH 203/245] change return object for broker observe function Signed-off-by: Brian Fletcher --- plugins/scaffolder-backend/api-report.md | 4 +++- .../src/scaffolder/tasks/StorageTaskBroker.ts | 4 ++-- plugins/scaffolder-backend/src/scaffolder/tasks/types.ts | 2 +- plugins/scaffolder-backend/src/service/router.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 6f9822cd09..3f9b77a3a3 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -369,7 +369,9 @@ export interface TaskBroker { events: SerializedTaskEvent[]; }, ) => void, - ): () => void; + ): { + unsubscribe: () => void; + }; // (undocumented) vacuumTasks(timeoutS: { timeoutS: number }): Promise; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 7845ca994a..7e7ebe5436 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -178,7 +178,7 @@ export class StorageTaskBroker implements TaskBroker { error: Error | undefined, result: { events: SerializedTaskEvent[] }, ) => void, - ): () => void { + ): { unsubscribe: () => void } { const { taskId } = options; let cancelled = false; @@ -205,7 +205,7 @@ export class StorageTaskBroker implements TaskBroker { } })(); - return unsubscribe; + return { unsubscribe }; } async vacuumTasks(timeoutS: { timeoutS: number }): Promise { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 6b3075844d..6cfd914bd2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -166,7 +166,7 @@ export interface TaskBroker { error: Error | undefined, result: { events: SerializedTaskEvent[] }, ) => void, - ): () => void; + ): { unsubscribe: () => void }; get(taskId: string): Promise; } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 814b235bf2..a78b04da78 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -258,7 +258,7 @@ export async function createRouter( }); // After client opens connection send all events as string - const unsubscribe = taskBroker.observe( + const { unsubscribe } = taskBroker.observe( { taskId, after }, (error, { events }) => { if (error) { From b45607a2ec899c7afba39f7d5987f1e064651b5a Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Thu, 28 Oct 2021 17:56:59 -0400 Subject: [PATCH 204/245] fix: make techdocs s3 credentials optional in config schema Signed-off-by: Andrew Thauer --- .changeset/bright-taxis-stare.md | 5 +++++ plugins/techdocs-backend/config.d.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/bright-taxis-stare.md diff --git a/.changeset/bright-taxis-stare.md b/.changeset/bright-taxis-stare.md new file mode 100644 index 0000000000..e43c9b038b --- /dev/null +++ b/.changeset/bright-taxis-stare.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Make techdocs s3 publisher credentials config schema optional. diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index ac31474fa9..0023a4b757 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -83,12 +83,12 @@ export interface Config { * User access key id * @visibility secret */ - accessKeyId: string; + accessKeyId?: string; /** * User secret access key * @visibility secret */ - secretAccessKey: string; + secretAccessKey?: string; /** * ARN of role to be assumed * @visibility backend From 4316c9632eb8dd65389bc9ab5ee01c91569fa4fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 04:09:55 +0000 Subject: [PATCH 205/245] build(deps): bump @azure/msal-node from 1.2.0 to 1.3.2 Bumps [@azure/msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js) from 1.2.0 to 1.3.2. - [Release notes](https://github.com/AzureAD/microsoft-authentication-library-for-js/releases) - [Commits](https://github.com/AzureAD/microsoft-authentication-library-for-js/compare/v1.2.0...v1.3.2) --- updated-dependencies: - dependency-name: "@azure/msal-node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 517d66ba0c..2531e2b221 100644 --- a/yarn.lock +++ b/yarn.lock @@ -266,13 +266,20 @@ dependencies: tslib "^2.0.0" -"@azure/msal-common@^4.0.0", "@azure/msal-common@^4.4.0": +"@azure/msal-common@^4.0.0": version "4.4.0" resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.4.0.tgz#818526042f78838ebc332fb735e7de64d8bccb45" integrity sha512-Qrs33Ctt2KM7NxArFPIUKc8UbIcm7zYxJFdJeQ9k7HKBhVk3e88CUz1Mw33cS/Jr+YA1H02OAzHg++bJ+4SFyQ== dependencies: debug "^4.1.1" +"@azure/msal-common@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-5.0.1.tgz#234030b340cc63575e84190b96a8a336b69c7698" + integrity sha512-CmPR3XM9+CGUu7V/+bAwDxyN6XqWJJhVLmv7utT3sbgay4l5roVXsD1t4wURTs8PwzxmmnJOrhvvGhoDxUW69g== + dependencies: + debug "^4.1.1" + "@azure/msal-node@1.0.0-beta.6": version "1.0.0-beta.6" resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz#da6bc3a3a861057c85586055960e069f162548ee" @@ -284,12 +291,12 @@ uuid "^8.3.0" "@azure/msal-node@^1.1.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.2.0.tgz#d08a5fb3391436715cb37c6eb44816013bdfa11e" - integrity sha512-79o5n483vslc7Qegh9+0BsxODRmlk6YYjVdl9jvwmAuF+i+oylq57e7RVhTVocKCbLCIMOKARI14JyKdDbW0WA== + version "1.3.2" + resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.3.2.tgz#17665397c04b9cad57b42eb6e21d5d6f35d9b83a" + integrity sha512-aKU2lVRKhZa1IJ/Za/Ir6qlythQ3FHz0g0px3SbM4iC1otyr3ANS4mIn/6fmkpZDIHc8eAgJh2KMep1Yn2zpig== dependencies: - "@azure/msal-common" "^4.4.0" - axios "^0.21.1" + "@azure/msal-common" "^5.0.1" + axios "^0.21.4" jsonwebtoken "^8.5.1" uuid "^8.3.0" @@ -9348,7 +9355,7 @@ axios-cached-dns-resolve@0.5.2: pino "^5.12.2" pino-pretty "^2.6.0" -axios@^0.21.1: +axios@^0.21.1, axios@^0.21.4: version "0.21.4" resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== From 547b902b68909cdfdbe1f9d403fa79828727e6cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 04:12:33 +0000 Subject: [PATCH 206/245] build(deps): bump @roadiehq/backstage-plugin-github-insights Bumps [@roadiehq/backstage-plugin-github-insights](https://github.com/RoadieHQ/roadie-backstage-plugins/tree/HEAD/plugins/frontend/backstage-plugin-github-insights) from 1.1.23 to 1.2.2. - [Release notes](https://github.com/RoadieHQ/roadie-backstage-plugins/releases) - [Changelog](https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/frontend/backstage-plugin-github-insights/docs/releases-widget.png) - [Commits](https://github.com/RoadieHQ/roadie-backstage-plugins/commits/HEAD/plugins/frontend/backstage-plugin-github-insights) --- updated-dependencies: - dependency-name: "@roadiehq/backstage-plugin-github-insights" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 517d66ba0c..914dc2c3a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5184,15 +5184,16 @@ react-use "^17.2.4" "@roadiehq/backstage-plugin-github-insights@^1.1.23": - version "1.1.23" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.1.23.tgz#bcf7424df5a659df39d5aa75f5fffd8e02c9437e" - integrity sha512-3HL9nEtlaE+gFmV0xkrtZGbWHbS26z+NHzEk2KIyV8oqReQ+57PRLTVcXCjFA+djkKfP2hpu5s7zp72aVb0v+w== + version "1.2.2" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.2.2.tgz#09d958ed15adbb598afda34187f45deedec2264d" + integrity sha512-ybomCU/3NIQJYahp/E78wl+JuzpwkRkhyeCcySLA45GaCz/mEqAGSs4xDZancpV2ls6nxV3mKUw/DL9afmMGAw== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.3.0" + "@backstage/core-components" "^0.7.0" "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.4.0" + "@backstage/integration-react" "^0.1.10" + "@backstage/plugin-catalog-react" "^0.6.0" "@backstage/theme" "^0.2.7" "@date-io/core" "2.10.7" "@material-ui/core" "^4.11.0" From 2406f82644610745be3e3b8a15e5d6e09f3a59a8 Mon Sep 17 00:00:00 2001 From: djamaile Date: Fri, 29 Oct 2021 10:32:04 +0200 Subject: [PATCH 207/245] docs(#7768): include dependsOn to depict a relation between a component and resource Signed-off-by: djamaile --- docs/features/software-catalog/descriptor-format.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index c174b94557..7100e86ed0 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -502,6 +502,8 @@ spec: lifecycle: production owner: artist-relations-team system: artist-engagement-portal + dependsOn: + - resource:default/artists-db providesApis: - artist-api ``` From 4eb087ccdcce9d81786e0ca4b2433f21f8d8b5a8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 11:03:53 +0200 Subject: [PATCH 208/245] techdocs-backend: release schema hotfix Signed-off-by: Patrik Oldsberg --- .changeset/bright-taxis-stare.md | 5 ----- packages/create-app/CHANGELOG.md | 2 ++ packages/create-app/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 6 ++++++ plugins/techdocs-backend/package.json | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 .changeset/bright-taxis-stare.md diff --git a/.changeset/bright-taxis-stare.md b/.changeset/bright-taxis-stare.md deleted file mode 100644 index e43c9b038b..0000000000 --- a/.changeset/bright-taxis-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch ---- - -Make techdocs s3 publisher credentials config schema optional. diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 05e067ce2c..95d99d0532 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,7 @@ # @backstage/create-app +## 0.4.2 + ## 0.4.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 725f865435..ad4ab8dabd 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.1", + "version": "0.4.2", "private": false, "publishConfig": { "access": "public" diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 103306f79b..7331ea0147 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-techdocs-backend +## 0.10.7 + +### Patch Changes + +- b45607a2ec: Make techdocs s3 publisher credentials config schema optional. + ## 0.10.6 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 3208b451fa..c71833948b 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.10.6", + "version": "0.10.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 6129c89a47ef0df51b58708b2f0ad4604bcc2f6b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 29 Oct 2021 11:48:39 +0200 Subject: [PATCH 209/245] Bump TechDocs container to v0.3.5. Signed-off-by: Eric Peterson --- .changeset/techdocs-buh-bump-ts.md | 5 +++++ packages/techdocs-common/api-report.md | 2 +- packages/techdocs-common/src/stages/generate/techdocs.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/techdocs-buh-bump-ts.md diff --git a/.changeset/techdocs-buh-bump-ts.md b/.changeset/techdocs-buh-bump-ts.md new file mode 100644 index 0000000000..438843e04b --- /dev/null +++ b/.changeset/techdocs-buh-bump-ts.md @@ -0,0 +1,5 @@ +--- +'@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). diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index f5a00c25ea..61371525e5 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -256,7 +256,7 @@ export class TechdocsGenerator implements GeneratorBase { config: Config; scmIntegrations: ScmIntegrationRegistry; }); - static readonly defaultDockerImage = 'spotify/techdocs:v0.3.4'; + static readonly defaultDockerImage = 'spotify/techdocs:v0.3.5'; // (undocumented) static fromConfig( config: Config, diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index e87278c2d4..728cab5a81 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -44,7 +44,7 @@ export class TechdocsGenerator implements GeneratorBase { * The default docker image (and version) used to generate content. Public * and static so that techdocs-common consumers can use the same version. */ - public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.4'; + public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.5'; private readonly logger: Logger; private readonly containerRunner: ContainerRunner; private readonly options: GeneratorConfig; From 75fc6ac85bbc3a6f73ef0cac9838ea17f9e23591 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Tue, 14 Sep 2021 12:54:52 +0200 Subject: [PATCH 210/245] Implement Tech Insights backend * Add common types and interfaces for Tech Insights. Exposing components needed to use and modify tech-insights-backend module and to implement individual Fact Retrievers, Fact Checkers and their persistence options. * Implement a framework to run fact retrievers and store fact data into the database. Add migration scripts to create a new database for `tech_insights`. * Create a default implementation of a FactChecker enabling users to construct checks and run them and generate scorecards based on checks. * To be able to use tech insights in your application you need to implement `FactRetriever`s to retrieve and return data for facts and register them to the tech-insights-backend. If you want to use fact checking functionality, you need to create `check`s and register them to an implementation of a `FactChecker`. For more information see documentation on the README.md files of the respective packages. Signed-off-by: Jussi Hallila --- .changeset/bright-pandas-rush.md | 17 + packages/backend/package.json | 3 + packages/backend/src/index.ts | 5 + packages/backend/src/plugins/techInsights.ts | 47 +++ .../.eslintrc.js | 3 + .../CHANGELOG.md | 7 + .../README.md | 83 +++++ .../api-report.md | 126 +++++++ .../package.json | 52 +++ .../src/index.ts | 32 ++ .../src/service/CheckRegistry.ts | 59 ++++ .../JsonRulesEngineFactChecker.test.ts | 293 ++++++++++++++++ .../src/service/JsonRulesEngineFactChecker.ts | 324 ++++++++++++++++++ .../src/service/validation-schema.json | 208 +++++++++++ .../src/setupTests.ts | 17 + .../src/types.ts | 87 +++++ plugins/tech-insights-backend/.eslintrc.js | 11 + plugins/tech-insights-backend/CHANGELOG.md | 7 + plugins/tech-insights-backend/README.md | 211 ++++++++++++ plugins/tech-insights-backend/api-report.md | 76 ++++ .../migrations/202109061111_fact_schemas.js | 57 +++ .../migrations/202109061212_facts.js | 73 ++++ plugins/tech-insights-backend/package.json | 64 ++++ plugins/tech-insights-backend/src/index.ts | 26 ++ .../src/service/DefaultTechInsightsBuilder.ts | 149 ++++++++ .../service/fact/FactRetrieverEngine.test.ts | 152 ++++++++ .../src/service/fact/FactRetrieverEngine.ts | 135 ++++++++ .../src/service/fact/FactRetrieverRegistry.ts | 63 ++++ .../service/persistence/DatabaseManager.ts | 99 ++++++ .../persistence/TechInsightsDatabase.test.ts | 218 ++++++++++++ .../persistence/TechInsightsDatabase.ts | 188 ++++++++++ .../src/service/router.test.ts | 125 +++++++ .../src/service/router.ts | 136 ++++++++ .../tech-insights-backend/src/setupTests.ts | 17 + plugins/tech-insights-common/.eslintrc.js | 3 + plugins/tech-insights-common/CHANGELOG.md | 7 + plugins/tech-insights-common/README.md | 3 + plugins/tech-insights-common/api-report.md | 167 +++++++++ plugins/tech-insights-common/package.json | 45 +++ plugins/tech-insights-common/src/checks.ts | 159 +++++++++ plugins/tech-insights-common/src/facts.ts | 196 +++++++++++ .../tech-insights-common/src/index.test.ts | 24 ++ plugins/tech-insights-common/src/index.ts | 20 ++ .../tech-insights-common/src/persistence.ts | 79 +++++ plugins/tech-insights-common/src/responses.ts | 100 ++++++ .../tech-insights-common/src/setupTests.ts | 17 + yarn.lock | 78 ++++- 47 files changed, 4065 insertions(+), 3 deletions(-) create mode 100644 .changeset/bright-pandas-rush.md create mode 100644 packages/backend/src/plugins/techInsights.ts create mode 100644 plugins/tech-insights-backend-module-jsonfc/.eslintrc.js create mode 100644 plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md create mode 100644 plugins/tech-insights-backend-module-jsonfc/README.md create mode 100644 plugins/tech-insights-backend-module-jsonfc/api-report.md create mode 100644 plugins/tech-insights-backend-module-jsonfc/package.json create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/index.ts create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/setupTests.ts create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/types.ts create mode 100644 plugins/tech-insights-backend/.eslintrc.js create mode 100644 plugins/tech-insights-backend/CHANGELOG.md create mode 100644 plugins/tech-insights-backend/README.md create mode 100644 plugins/tech-insights-backend/api-report.md create mode 100644 plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js create mode 100644 plugins/tech-insights-backend/migrations/202109061212_facts.js create mode 100644 plugins/tech-insights-backend/package.json create mode 100644 plugins/tech-insights-backend/src/index.ts create mode 100644 plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts create mode 100644 plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts create mode 100644 plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts create mode 100644 plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts create mode 100644 plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts create mode 100644 plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts create mode 100644 plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts create mode 100644 plugins/tech-insights-backend/src/service/router.test.ts create mode 100644 plugins/tech-insights-backend/src/service/router.ts create mode 100644 plugins/tech-insights-backend/src/setupTests.ts create mode 100644 plugins/tech-insights-common/.eslintrc.js create mode 100644 plugins/tech-insights-common/CHANGELOG.md create mode 100644 plugins/tech-insights-common/README.md create mode 100644 plugins/tech-insights-common/api-report.md create mode 100644 plugins/tech-insights-common/package.json create mode 100644 plugins/tech-insights-common/src/checks.ts create mode 100644 plugins/tech-insights-common/src/facts.ts create mode 100644 plugins/tech-insights-common/src/index.test.ts create mode 100644 plugins/tech-insights-common/src/index.ts create mode 100644 plugins/tech-insights-common/src/persistence.ts create mode 100644 plugins/tech-insights-common/src/responses.ts create mode 100644 plugins/tech-insights-common/src/setupTests.ts diff --git a/.changeset/bright-pandas-rush.md b/.changeset/bright-pandas-rush.md new file mode 100644 index 0000000000..86b3b819ee --- /dev/null +++ b/.changeset/bright-pandas-rush.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-tech-insights-backend': minor +'@backstage/plugin-tech-insights-backend-module-jsonfc': minor +'@backstage/plugin-tech-insights-common': minor +--- + +## Add initial implementation of Tech Insights backend + +Add common types and interfaces for Tech Insights. Exposing components needed to use and modify tech-insights-backend module and to implement individual Fact Retrievers, Fact Checkers and their persistence options. + +Implement a framework to run fact retrievers and store fact data into the database. Add migration scripts to create a new database for `tech_insights`. + +Create a default implementation of a FactChecker enabling users to construct checks and run them and generate scorecards based on checks. + +To be able to use tech insights in your application you need to implement `FactRetriever`s to retrieve and return data for facts and register them to the tech-insights-backend. If you want to use fact checking functionality, you need to create `check`s and register them to an implementation of a `FactChecker`. + +For more information see documentation on the README.md files of the respective packages. diff --git a/packages/backend/package.json b/packages/backend/package.json index b9a28c2853..a56b302954 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -48,6 +48,9 @@ "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4", "@backstage/plugin-search-backend-module-pg": "^0.2.1", "@backstage/plugin-techdocs-backend": "^0.10.5", + "@backstage/plugin-tech-insights-backend": "^0.1.0", + "@backstage/plugin-tech-insights-common": "^0.1.0", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.0", "@backstage/plugin-todo-backend": "^0.1.13", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b6c148dfba..6d40072f99 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -53,6 +53,7 @@ import graphql from './plugins/graphql'; import app from './plugins/app'; import badges from './plugins/badges'; import jenkins from './plugins/jenkins'; +import techInsights from './plugins/techInsights'; import { PluginEnvironment } from './types'; function makeCreateEnv(config: Config) { @@ -107,12 +108,16 @@ async function main() { const appEnv = useHotMemoize(module, () => createEnv('app')); const badgesEnv = useHotMemoize(module, () => createEnv('badges')); const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); + const techInsightsEnv = useHotMemoize(module, () => + createEnv('tech_insights'), + ); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv)); apiRouter.use('/rollbar', await rollbar(rollbarEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv)); apiRouter.use('/search', await search(searchEnv)); diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts new file mode 100644 index 0000000000..c396f4b82b --- /dev/null +++ b/packages/backend/src/plugins/techInsights.ts @@ -0,0 +1,47 @@ +/* + * 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 { + createRouter, + DefaultTechInsightsBuilder, +} from '@backstage/plugin-tech-insights-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + +export default async function createPlugin({ + logger, + config, + discovery, + database, +}: PluginEnvironment): Promise { + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [], + factCheckerFactory: new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, + }), + }); + + return await createRouter({ + ...(await builder.build()), + logger, + config, + }); +} diff --git a/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md new file mode 100644 index 0000000000..d0aced5bf5 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-tech-insights-backend-module-jsonfc + +## 0.0.1 + +### Patch Changes + +- Initial implementation diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md new file mode 100644 index 0000000000..ea45a1b4b0 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -0,0 +1,83 @@ +# Tech Insights Backend JSON Rules engine fact checker module + +This is an extension to module to tech-insights-backend plugin, which provides basic framework and functionality to implement tech insights within Backstage. + +This module provides functionality to run checks against a `json-rules-engine` and provide boolean logic by simply building checks using JSON conditions. + +## Getting started + +To add this FactChecker into your Tech Insights you need to install the module into your backend application: + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-tech-insights-backend-module-jsonfc +``` + +and modify the `techInsights.ts` file to contain a reference to the FactCheckers implementation. + +```diff ++ import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + ++ const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ ++ checks: [], ++ logger, ++ }), + +const builder = new DefaultTechInsightsBuilder({ +logger, +config, +database, +discovery, +factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory +}); +``` + +By default this implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows + +```diff +const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip +const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, ++ checkRegistry: myTechInsightCheckRegistry +}), + +``` + +## Adding checks + +Checks for this FactChecker are constructed as `json-rules-engine` compatible JSON rules. A check could look like the following for example: + +```ts +import { TechInsightJsonRuleCheck } from '../types'; + +export const exampleCheck: TechInsightJsonRuleCheck = { + id: 'demodatacheck', // Unique identifier of this check + name: 'demodatacheck', // A human readable name of this check to be displayed in the UI + description: 'A fact check for demoing purposes', // A description to be displayed in the UI + factRefs: ['demo-poc.factretriever'], // References to fact containers that this check uses. See documentation on FactRetrievers for more information on these + rule: { + // The actual rule + conditions: { + all: [ + // 2 options are available, all and any conditions. + { + fact: 'examplenumberfact', // Reference to an individual fact to check against + operator: 'greaterThanInclusive', // Operator to use. See: https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#operators for more + value: 2, // The threshold value that the fact must satisfy + }, + ], + }, + }, + successMetadata: { + // Additional metadata to be returned if the check has passed + link: 'https://link.to.some.information.com', + }, + failureMetadata: { + // Additional metadata to be returned if the check has failed + link: 'https://sonar.mysonarqube.com/increasing-number-value', + }, +}; +``` diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md new file mode 100644 index 0000000000..bf08028a00 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -0,0 +1,126 @@ +## API Report File for "@backstage/plugin-tech-insights-backend-module-jsonfc" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; +import { CheckResponse } from '@backstage/plugin-tech-insights-common'; +import { DynamicFactCallback } from 'json-rules-engine'; +import { FactChecker } from '@backstage/plugin-tech-insights-common'; +import { FactOptions } from 'json-rules-engine'; +import { Logger as Logger_2 } from 'winston'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-common'; +import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-common'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { TopLevelCondition } from 'json-rules-engine'; + +// @public (undocumented) +export type CheckCondition = { + operator: string; + fact: string; + factValue: any; + factResult: any; + result: boolean; +}; + +// @public (undocumented) +export interface DynamicFact { + // (undocumented) + calculationMethod: DynamicFactCallback | T; + // (undocumented) + id: string; + // (undocumented) + options?: FactOptions; +} + +// @public (undocumented) +export interface JsonRuleBooleanCheckResult extends BooleanCheckResult { + // (undocumented) + check: JsonRuleCheckResponse; +} + +// @public (undocumented) +export interface JsonRuleCheckResponse extends CheckResponse { + // (undocumented) + rule: { + conditions: ResponseTopLevelCondition & { + priority: number; + }; + }; +} + +// @public +export class JsonRulesEngineFactChecker + implements FactChecker +{ + constructor({ + checks, + repository, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerOptions); + // (undocumented) + addCheck(check: TechInsightJsonRuleCheck): Promise; + // (undocumented) + getChecks(): Promise; + // (undocumented) + runChecks( + entity: string, + checks: string[], + ): Promise; + // (undocumented) + validate(check: TechInsightJsonRuleCheck): Promise; +} + +// @public +export class JsonRulesEngineFactCheckerFactory { + constructor({ + checks, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerFactoryOptions); + // (undocumented) + construct(repository: TechInsightsStore): JsonRulesEngineFactChecker; +} + +// @public +export type JsonRulesEngineFactCheckerFactoryOptions = { + checks: TechInsightJsonRuleCheck[]; + logger: Logger_2; + checkRegistry?: TechInsightCheckRegistry; +}; + +// @public +export type JsonRulesEngineFactCheckerOptions = { + checks: TechInsightJsonRuleCheck[]; + repository: TechInsightsStore; + logger: Logger_2; + checkRegistry?: TechInsightCheckRegistry; +}; + +// @public (undocumented) +export type ResponseTopLevelCondition = + | { + all: CheckCondition[]; + } + | { + any: CheckCondition[]; + }; + +// @public (undocumented) +export type Rule = { + conditions: TopLevelCondition; + name?: string; + priority?: number; +}; + +// @public (undocumented) +export interface TechInsightJsonRuleCheck extends TechInsightCheck { + // (undocumented) + dynamicFacts?: DynamicFact[]; + // (undocumented) + rule: Rule; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json new file mode 100644 index 0000000000..945947dd3b --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "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": "plugins/tech-insights-backend-module-jsonfc" + }, + "keywords": [ + "backstage", + "tech-insights", + "scorecard" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "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.0", + "@backstage/config": "^0.1.8", + "@backstage/errors": "^0.1.1", + "@backstage/plugin-tech-insights-common": "^0.1.0", + "ajv": "^7.0.3", + "json-rules-engine": "^6.1.2", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "node-cron": "^3.0.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.7.1", + "@types/node-cron": "^2.0.4" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/index.ts new file mode 100644 index 0000000000..a2561eaa3b --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { + JsonRulesEngineFactCheckerFactory, + JsonRulesEngineFactChecker, +} from './service/JsonRulesEngineFactChecker'; +export type { + JsonRulesEngineFactCheckerFactoryOptions, + JsonRulesEngineFactCheckerOptions, +} from './service/JsonRulesEngineFactChecker'; +export type { + JsonRuleCheckResponse, + JsonRuleBooleanCheckResult, + TechInsightJsonRuleCheck, + ResponseTopLevelCondition, + Rule, + DynamicFact, + CheckCondition, +} from './types'; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts new file mode 100644 index 0000000000..2ae1ac0b83 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts @@ -0,0 +1,59 @@ +/* + * 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, NotFoundError } from '@backstage/errors'; +import { + TechInsightCheck, + TechInsightCheckRegistry, +} from '@backstage/plugin-tech-insights-common'; + +export class DefaultCheckRegistry + implements TechInsightCheckRegistry +{ + private readonly checks = new Map(); + + constructor(checks: CheckType[]) { + checks.forEach(check => { + this.register(check); + }); + } + + async register(check: CheckType) { + if (this.checks.has(check.id)) { + throw new ConflictError( + `Tech insight check with id ${check.id} has already been registered`, + ); + } + this.checks.set(check.id, check); + } + + async get(checkId: string): Promise { + const check = this.checks.get(checkId); + if (!check) { + throw new NotFoundError( + `Tech insight check with id '${checkId}' is not registered.`, + ); + } + return check; + } + async getAll(checks: string[]): Promise { + return Promise.all(checks.map(checkId => this.get(checkId))); + } + + async list(): Promise { + return [...this.checks.values()]; + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts new file mode 100644 index 0000000000..d574e13c5f --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -0,0 +1,293 @@ +/* + * 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 { + TechInsightCheckRegistry, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-common'; +import { JsonRulesEngineFactCheckerFactory } from '../index'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TechInsightJsonRuleCheck } from '../types'; + +const testChecks: Record = { + broken: [ + { + id: 'brokenTestCheck', + name: 'brokenTestCheck', + description: 'Broken Check For Testing', + factRefs: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + operator: 'largerThan', + value: 1, + }, + ], + }, + }, + }, + ], + broken2: [ + { + id: 'brokenTestCheck2', + name: 'brokenTestCheck2', + description: 'Second Broken Check For Testing', + factRefs: ['non-existing-factretriever'], + rule: { + conditions: { + any: [ + { + fact: 'somefact', + operator: 'lessThan', + value: 1, + }, + ], + }, + }, + }, + ], + simple: [ + { + id: 'simpleTestCheck', + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factRefs: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + operator: 'lessThan', + value: 5, + }, + ], + }, + }, + }, + ], + + simple2: [ + { + id: 'simpleTestCheck2', + name: 'simpleTestCheck2', + description: 'Second Simple Check For Testing', + factRefs: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + operator: 'greaterThan', + value: 2, + }, + ], + }, + }, + }, + ], +}; + +const latestSchemasMock = jest.fn().mockImplementation(() => [ + { + version: '0.0.1', + id: 2, + ref: 'test-factretriever', + schema: { + testnumberfact: { + type: 'integer', + description: '', + entityKinds: ['component'], + }, + }, + }, +]); +const factsBetweenTimestampsForRefsMock = jest.fn(); +const latestFactsForRefsMock = jest.fn().mockImplementation(() => ({})); +const mockCheckRegistry = { + getAll(checks: string[]) { + return checks.flatMap(check => testChecks[check]); + }, +} as unknown as TechInsightCheckRegistry; + +const mockRepository: TechInsightsStore = { + getLatestFactsForRefs: latestFactsForRefsMock, + getFactsBetweenTimestampsForRefs: factsBetweenTimestampsForRefsMock, + getLatestSchemas: latestSchemasMock, +} as unknown as TechInsightsStore; + +describe('JsonRulesEngineFactChecker', () => { + const factChecker = new JsonRulesEngineFactCheckerFactory({ + checkRegistry: mockCheckRegistry, + checks: [], + logger: getVoidLogger(), + }).construct(mockRepository); + + describe('when running checks', () => { + it('should throw on incorrectly configured checks conditions', async () => { + const cur = async () => await factChecker.runChecks('a/a/a', ['broken']); + await expect(cur()).rejects.toThrowError( + 'Failed to run rules engine, Unknown operator: largerThan', + ); + }); + + it('should handle cases where wrong facts are referenced', async () => { + const cur = async () => await factChecker.runChecks('a/a/a', ['broken2']); + await expect(cur()).rejects.toThrowError( + 'Failed to run rules engine, Undefined fact: somefact', + ); + }); + it('should respond with result, facts, fact schemas and checks', async () => { + latestFactsForRefsMock.mockImplementation(() => + Promise.resolve({ + ['test-factretriever']: { + ref: 'test-factretriever', + facts: { + testnumberfact: 3, + }, + }, + }), + ); + const results = await factChecker.runChecks('a/a/a', ['simple']); + expect(results).toMatchObject([ + { + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', + entityKinds: ['component'], + }, + }, + result: true, + check: { + id: 'simpleTestCheck', + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factRefs: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + factResult: 3, + operator: 'lessThan', + result: true, + value: 5, + }, + ], + priority: 1, + }, + }, + }, + }, + ]); + }); + + it('should gracefully handle multiple check at once', async () => { + latestFactsForRefsMock.mockImplementation(() => + Promise.resolve({ + ['test-factretriever']: { + ref: 'test-factretriever', + facts: { + testnumberfact: 3, + }, + }, + }), + ); + const results = await factChecker.runChecks('a/a/a', [ + 'simple', + 'simple2', + ]); + expect(results).toMatchObject([ + { + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', + entityKinds: ['component'], + }, + }, + result: true, + check: { + id: 'simpleTestCheck', + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factRefs: ['test-factretriever'], + rule: { + conditions: { + priority: 1, + all: [ + { + operator: 'lessThan', + value: 5, + fact: 'testnumberfact', + factResult: 3, + result: true, + }, + ], + }, + }, + }, + }, + { + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', + entityKinds: ['component'], + }, + }, + result: true, + check: { + id: 'simpleTestCheck2', + name: 'simpleTestCheck2', + description: 'Second Simple Check For Testing', + factRefs: ['test-factretriever'], + rule: { + conditions: { + priority: 1, + all: [ + { + operator: 'greaterThan', + value: 2, + fact: 'testnumberfact', + factResult: 3, + result: true, + }, + ], + }, + }, + }, + }, + ]); + }); + }); + + describe('when validating checks', () => { + it('should succeed on valid rules', async () => { + const isValid = await factChecker.validate(testChecks.simple[0]); + expect(isValid).toBeTruthy(); + }); + it('should fail on broken rules', async () => { + const isValid = await factChecker.validate(testChecks.broken[0]); + expect(isValid).toBeFalsy(); + }); + }); +}); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts new file mode 100644 index 0000000000..964575b494 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -0,0 +1,324 @@ +/* + * 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 { JsonRuleBooleanCheckResult, TechInsightJsonRuleCheck } from '../types'; +import { + FactChecker, + FactResponse, + FactValueDefinitions, + TechInsightCheckRegistry, + FlatTechInsightFact, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-common'; +import { Engine, EngineResult, TopLevelCondition } from 'json-rules-engine'; +import { DefaultCheckRegistry } from './CheckRegistry'; +import { Logger } from 'winston'; +import { pick } from 'lodash'; +import Ajv from 'ajv'; +import * as validationSchema from './validation-schema.json'; + +const noopEvent = { + type: 'noop', +}; + +/** + * @public + * Should actually be at-internal + * + * Constructor options for JsonRulesEngineFactChecker + */ +export type JsonRulesEngineFactCheckerOptions = { + checks: TechInsightJsonRuleCheck[]; + repository: TechInsightsStore; + logger: Logger; + checkRegistry?: TechInsightCheckRegistry; +}; + +/** + * @public + * Should actually be at-internal + * + * FactChecker implementation using json-rules-engine + */ +export class JsonRulesEngineFactChecker + implements FactChecker +{ + private readonly checkRegistry: TechInsightCheckRegistry; + private repository: TechInsightsStore; + private readonly logger: Logger; + + constructor({ + checks, + repository, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerOptions) { + this.repository = repository; + this.logger = logger; + checks.forEach(check => this.validate(check)); + this.checkRegistry = checkRegistry ?? new DefaultCheckRegistry(checks); + } + + async runChecks( + entity: string, + checks: string[], + ): Promise { + const engine = new Engine(); + const techInsightChecks = await this.checkRegistry.getAll(checks); + const factRefs = techInsightChecks.flatMap(it => it.factRefs); + const facts = await this.repository.getLatestFactsForRefs(factRefs, entity); + techInsightChecks.forEach(techInsightCheck => { + const rule = techInsightCheck.rule; + rule.name = techInsightCheck.id; + engine.addRule({ ...techInsightCheck.rule, event: noopEvent }); + + if (techInsightCheck.dynamicFacts) { + techInsightCheck.dynamicFacts.forEach(it => + engine.addFact(it.id, it.calculationMethod, it.options), + ); + } + }); + + const factValues = Object.values(facts).reduce( + (acc, it) => ({ ...acc, ...it.facts }), + {}, + ); + + try { + const results = await engine.run(factValues); + return await this.ruleEngineResultsToCheckResponse( + results, + techInsightChecks, + Object.values(facts), + ); + } catch (e) { + throw new Error(`Failed to run rules engine, ${e.message}`); + } + } + + async validate(check: TechInsightJsonRuleCheck): Promise { + const ajv = new Ajv({ verbose: true }); + const validator = ajv.compile(validationSchema); + const isValidToSchema = validator(check.rule); + if (!isValidToSchema) { + this.logger.warn( + 'Failed to to validate conditions against JSON schema', + validator.errors, + ); + return false; + } + + const existingSchemas = await this.repository.getLatestSchemas( + check.factRefs, + ); + const references = this.retrieveFactReferences(check.rule.conditions); + const results = references.map(ref => ({ + ref, + result: existingSchemas.some(schema => schema.schema.hasOwnProperty(ref)), + })); + const failedReferences = results.filter(it => !it.result); + failedReferences.forEach(it => { + this.logger.warn( + `Validation failed for check ${check.name}. Reference to value ${ + it.ref + } does not exists in referred fact schemas: ${check.factRefs.join( + ',', + )}`, + ); + }); + return failedReferences.length === 0; + } + + getChecks(): Promise { + return this.checkRegistry.list(); + } + + async addCheck(check: TechInsightJsonRuleCheck): Promise { + if (!(await this.validate(check))) { + this.logger.warn( + `Check validation failed when adding check ${check.name} to check registry.`, + ); + return false; + } + this.checkRegistry.register(check); + return true; + } + + private retrieveFactReferences( + condition: TopLevelCondition | { fact: string }, + ): string[] { + let results: string[] = []; + if ('all' in condition) { + results = results.concat( + condition.all.flatMap(con => this.retrieveFactReferences(con)), + ); + } else if ('any' in condition) { + results = results.concat( + condition.any.flatMap(con => this.retrieveFactReferences(con)), + ); + } else { + results.push(condition.fact); + } + return results; + } + + private async ruleEngineResultsToCheckResponse( + results: EngineResult, + techInsightChecks: TechInsightJsonRuleCheck[], + facts: FlatTechInsightFact[], + ) { + return await Promise.all( + [ + ...(results.results && results.results), + ...(results.failureResults && results.failureResults), + ].map(async result => { + const techInsightCheck = techInsightChecks.find( + check => check.id === result.name, + ); + if (!techInsightCheck) { + // This should never happen, we just constructed these based on each other + throw new Error( + `Failed to determine tech insight check with id ${result.name}. Discrepancy between ran rule engine and configured checks.`, + ); + } + const factResponse = await this.constructFactInformationResponse( + facts, + techInsightCheck, + ); + return { + facts: factResponse, + result: result.result, + check: JsonRulesEngineFactChecker.constructCheckResponse( + techInsightCheck, + result, + ), + }; + }), + ); + } + + private static constructCheckResponse( + techInsightCheck: TechInsightJsonRuleCheck, + result: any, + ) { + const returnable = { + id: techInsightCheck.id, + name: techInsightCheck.name, + description: techInsightCheck.description, + factRefs: techInsightCheck.factRefs, + metadata: result.result + ? techInsightCheck.successMetadata + : techInsightCheck.failureMetadata, + rule: { conditions: {} }, + }; + + if ('toJSON' in result) { + // Results serialize "wrong" since the objects are creating their own serialization implementations + // 'toJSON' should always be present in the result object but it is missing from the types + const rule = JSON.parse(result.toJSON()); + return { ...returnable, rule: pick(rule, ['conditions']) }; + } + return returnable; + } + + private async constructFactInformationResponse( + facts: FlatTechInsightFact[], + techInsightCheck: TechInsightJsonRuleCheck, + ): Promise { + const factSchemas = await this.repository.getLatestSchemas( + techInsightCheck.factRefs, + ); + const schemas: FactValueDefinitions = factSchemas.reduce( + (acc, schema) => ({ ...acc, ...schema.schema }), + {}, + ); + const individualFacts = this.retrieveFactReferences( + techInsightCheck.rule.conditions, + ); + const factValues = facts + .filter(factContainer => + techInsightCheck.factRefs.includes(factContainer.ref), + ) + .reduce( + (acc, factContainer) => ({ + ...acc, + ...pick(factContainer.facts, individualFacts), + }), + {}, + ); + return Object.entries(factValues).reduce((acc, [key, value]) => { + return { + ...acc, + [key]: { + value, + ...schemas[key], + }, + }; + }, {}); + } +} + +/** + * @public + * + * Constructor options for JsonRulesEngineFactCheckerFactory + * + * Implementation of checkRegistry is optional. + * If there is a need to use persistent storage for checks, it is recommended to inject a storage implementation here. + * Otherwise an in-memory option is instantiated and used. + */ +export type JsonRulesEngineFactCheckerFactoryOptions = { + checks: TechInsightJsonRuleCheck[]; + logger: Logger; + checkRegistry?: TechInsightCheckRegistry; +}; + +/** + * @public + * + * Factory to construct JsonRulesEngineFactChecker + * Can be constructed with optional implementation of CheckInsightCheckRegistry if needed. + * Otherwise defaults to using in-memory CheckRegistry + */ +export class JsonRulesEngineFactCheckerFactory { + private readonly checks: TechInsightJsonRuleCheck[]; + private readonly logger: Logger; + private readonly checkRegistry?: TechInsightCheckRegistry; + + constructor({ + checks, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerFactoryOptions) { + this.logger = logger; + this.checks = checks; + this.checkRegistry = checkRegistry; + } + + /** + * @param repository - Implementation of TechInsightsStore. Used by the returned JsonRulesEngineFactChecker + * to retrieve fact and fact schema data + * @returns JsonRulesEngineFactChecker implementation + */ + construct(repository: TechInsightsStore) { + return new JsonRulesEngineFactChecker({ + checks: this.checks, + logger: this.logger, + checkRegistry: this.checkRegistry, + repository, + }); + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json b/plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json new file mode 100644 index 0000000000..9020ba0735 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json @@ -0,0 +1,208 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/CacheControl/json-rules-engine/rule-schema.json", + "type": "object", + "title": "Fact Checks", + "description": "Checks contain a set of conditions and a single event. When the engine is run, each check condition is evaluated and results returned", + "required": ["conditions"], + "properties": { + "conditions": { + "$ref": "#/definitions/conditions" + }, + "priority": { + "$id": "#/properties/priority", + "anyOf": [ + { + "type": "integer", + "minimum": 1 + } + ], + "title": "Priority", + "description": "Dictates when check should be run, relative to other check. Higher priority checks are run before lower priority checks. Checks with the same priority are run in parallel. Priority must be a positive, non-zero integer.", + "default": 1, + "examples": [1] + } + }, + "definitions": { + "conditions": { + "type": "object", + "title": "Conditions", + "description": "Check conditions are a combination of facts, operators, and values that determine whether the check is a success or a failure. The simplest form of a condition consists of a fact, an operator, and a value. When the engine runs, the operator is used to compare the fact against the value. Each check's conditions must have either an all or an any operator at its root, containing an array of conditions. The all operator specifies that all conditions contained within must be truthy for the check to be considered a success. The any operator only requires one condition to be truthy for the check to succeed.", + "default": {}, + "examples": [ + { + "all": [ + { + "value": true, + "fact": "displayMessage", + "operator": "equal" + } + ] + } + ], + "oneOf": [ + { + "required": ["any"] + }, + { + "required": ["all"] + } + ], + "properties": { + "any": { + "$ref": "#/definitions/conditionArray" + }, + "all": { + "$ref": "#/definitions/conditionArray" + } + } + }, + "conditionArray": { + "type": "array", + "title": "Condition Array", + "description": "An array of conditions with a possible recursive inclusion of another condition array.", + "default": [], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/conditions" + }, + { + "$ref": "#/definitions/condition" + } + ] + } + }, + "condition": { + "type": "object", + "title": "Condition", + "description": "Check conditions are a combination of facts, operators, and values that determine whether the check is a success or a failure. The simplest form of a condition consists of a fact, an operator, and a value. When the engine runs, the operator is used to compare the fact against the value.", + "default": { + "fact": "my-fact", + "operator": "lessThanInclusive", + "value": 1 + }, + "examples": [ + { + "fact": "gameDuration", + "operator": "equal", + "value": 40.0 + }, + { + "value": 5.0, + "fact": "personalFoulCount", + "operator": "greaterThanInclusive" + }, + { + "fact": "product-price", + "operator": "greaterThan", + "path": "$.price", + "value": 100.0, + "params": { + "productId": "widget" + } + } + ], + "required": ["fact", "operator", "value"], + "properties": { + "fact": { + "type": "string", + "title": "Fact", + "description": "Facts are methods or constants registered with the engine prior to runtime and referenced within check conditions. Each fact method should be a pure function that may return a either computed value, or promise that resolves to a computed value. As check conditions are evaluated during runtime, they retrieve fact values dynamically and use the condition operator to compare the fact result with the condition value.", + "default": "", + "examples": ["gameDuration"] + }, + "operator": { + "type": "string", + "anyOf": [ + { + "const": "equal", + "title": "fact must equal value" + }, + { + "const": "notEqual", + "title": "fact must not equal value" + }, + { + "const": "lessThan", + "title": "fact must be less than value" + }, + { + "const": "lessThanInclusive", + "title": "fact must be less than or equal to value" + }, + { + "const": "greaterThan", + "title": "fact must be greater than value" + }, + { + "const": "greaterThanInclusive", + "title": "fact must be greater than or equal to value" + }, + { + "const": "in", + "title": "fact must be included in value (an array)" + }, + { + "const": "notIn", + "title": "fact must not be included in value (an array)" + }, + { + "const": "contains", + "title": "fact (an array) must include value" + }, + { + "const": "doesNotContain", + "title": "fact (an array) must not include value" + } + ], + "title": "Operator", + "description": "The operator compares the value returned by the fact to what is stored in the value property. If the result is truthy, the condition passes.", + "default": "", + "examples": ["equal"] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "array" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "title": "Value", + "description": "The value the fact should be compared to.", + "default": 0, + "examples": [40] + }, + "params": { + "type": "object", + "title": "Event Params", + "description": "Optional helper params to make available to the event processor.", + "default": {}, + "examples": [ + { + "customProperty": "customValue" + } + ] + }, + "path": { + "type": "string", + "title": "Path", + "description": "For more complex data structures, writing a separate fact handler for each object property quickly becomes verbose and unwieldy. To address this, a path property may be provided to traverse fact data using json-path syntax. Json-path support is provided by jsonpath-plus", + "default": "", + "examples": ["$.price"] + } + } + } + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/setupTests.ts b/plugins/tech-insights-backend-module-jsonfc/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 {}; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/types.ts b/plugins/tech-insights-backend-module-jsonfc/src/types.ts new file mode 100644 index 0000000000..5f6210d110 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/types.ts @@ -0,0 +1,87 @@ +/* + * 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 { + DynamicFactCallback, + FactOptions, + TopLevelCondition, +} from 'json-rules-engine'; +import { + BooleanCheckResult, + CheckResponse, + TechInsightCheck, +} from '@backstage/plugin-tech-insights-common'; + +/** + * @public + */ +export interface DynamicFact { + id: string; + calculationMethod: DynamicFactCallback | T; + options?: FactOptions; +} + +/** + * @public + */ +export type Rule = { + conditions: TopLevelCondition; + name?: string; + priority?: number; +}; + +/** + * @public + */ +export interface TechInsightJsonRuleCheck extends TechInsightCheck { + rule: Rule; + dynamicFacts?: DynamicFact[]; +} + +/** + * @public + */ +export type CheckCondition = { + operator: string; + fact: string; + factValue: any; + factResult: any; + result: boolean; +}; + +/** + * @public + */ +export type ResponseTopLevelCondition = + | { all: CheckCondition[] } + | { any: CheckCondition[] }; + +/** + * @public + */ +export interface JsonRuleCheckResponse extends CheckResponse { + rule: { + conditions: ResponseTopLevelCondition & { + priority: number; + }; + }; +} + +/** + * @public + */ +export interface JsonRuleBooleanCheckResult extends BooleanCheckResult { + check: JsonRuleCheckResponse; +} diff --git a/plugins/tech-insights-backend/.eslintrc.js b/plugins/tech-insights-backend/.eslintrc.js new file mode 100644 index 0000000000..a126c438a4 --- /dev/null +++ b/plugins/tech-insights-backend/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], + rules: { + 'jest/expect-expect': [ + 'error', + { + assertFunctionNames: ['expect', 'request.**.expect'], + }, + ], + }, +}; diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md new file mode 100644 index 0000000000..17e0211ab0 --- /dev/null +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-tech-insights-backend + +## 0.0.1 + +### Patch Changes + +- Initial implementation diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md new file mode 100644 index 0000000000..4e5b5ad1b3 --- /dev/null +++ b/plugins/tech-insights-backend/README.md @@ -0,0 +1,211 @@ +# Tech Insights Backend + +This is the backend for the default Backstage Tech Insights feature. +This provides the API for the frontend tech insights, scorecards and fact visualization functionality, +as well as a framework to run fact retrievers and store fact values in to a data store. + +## Installation + +### Install the package + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-tech-insights-backend +``` + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can +do this by creating a file called `packages/backend/src/plugins/techInsights.ts`. An example content for `techInsights.ts` could be something like this. + +```ts +import { + createRouter, + DefaultTechInsightsBuilder, +} from '@backstage/plugin-tech-insights-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, + discovery, + database, +}: PluginEnvironment): Promise { + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [], // Fact retrievers registrations you want tech insights to use + }); + + return await createRouter({ + ...(await builder.build()), + logger, + config, + }); +} +``` + +With the `techInsights.ts` router setup in place, add the router to +`packages/backend/src/index.ts`: + +```diff ++import techInsights from './plugins/techInsights'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const techInsightsEnv = useHotMemoize(module, () => createEnv('tech_insights')); + + const apiRouter = Router(); ++ apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); + ... + apiRouter.use(notFoundHandler()); + +``` + +### Adding fact retrievers + +At this point the Tech Insights backend is installed in your backend package, but +you will not have any fact retrievers present in your application. To have the implemented FactRetrieverEngine within this package to be able to retrieve and store fact data into the database, you need to add these. + +To create factRetrieverRegistration you need to implement `FactRetriever` interface defined in `@backstage/plugin-tech-insights-common` package. After you have implemented this interface you can wrap that into a registration object like follows: + +```ts +const myFactRetriever: FactRetriever = { + /** + * snip + */ +}; + +const myFactRetrieverRegistration = { + cadence: '1 * 3 * * ', // On the first minute of the third day of the month + factRetriever: myFactRetriever, +}; +``` + +Then you can modify the example `techInsights.ts` file shown above like this: + +```diff +const builder = new DefaultTechInsightsBuilder({ +logger, +config, +database, +discovery, +- factRetrievers: [], ++ factRetrievers: [myFactRetrieverRegistration], +}); +``` + +### Creating Fact Retrievers + +A Fact Retriever consist of three parts: + +1. `ref` - unique identifier of a fact retriever +2. `schema` - A versioned schema defining the shape of data a fact retriever returns +3. `handler` - An asynchronous function handling the logic of retrieving and returning facts for an entity + +An example implementation of a FactRetriever could for example be as follows: + +```ts +const myFactRetriever: FactRetriever = { + ref: 'documentation-number-factretriever', // unique ref, identifier of the fact retriever + schema: { + version: '0.1.1', // SemVer version number of this fact retriever schema. This should be incremented if the implementation changes + + // the actual schema + schema: { + // Name/identifier of an individual fact that this retriever returns + examplenumberfact: { + type: 'integer', // Type of the fact + description: 'A fact of a number', // Description of the fact + entityKinds: ['component'], // An array of entity kinds that this fact is applicable to + }, + }, + }, + handler: async ctx => { + // Handler function that retrieves the fact + const { discovery, config, logger } = ctx; + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities(); // Retrieve all entities + /** + * snip: Do complex logic to retrieve facts from external system or calculate fact values + */ + + // Respond with an array of entity/fact values + return entities.items.map(it => { + return { + // Entity information that this fact relates to + entity: { + namespace: it.metadata.namespace, + kind: it.kind, + name: it.metadata.name, + }, + + // All facts that this retriever returns + facts: { + examplenumberfact: 2, // + }, + // (optional) timestamp to use as a Luxon DateTime object + }; + }); + }, +}; +``` + +### Adding a fact checker + +This module comes with a possibility to additionally add a fact checker and expose fact checking endpoints from the API. To be able to enable this feature you need to add a FactCheckerFactory implementation to be part of the `DefaultTechInsightsBuilder` constructor call. + +There is a default FactChecker implementation provided in module `@backstage/plugin-tech-insights-backend-module-jsonfc`. This implementation uses `json-rules-engine` as the underlying functionality to run checks. If you want to implement your own FactChecker, for example to be able to handle other than `boolean` result types, you can do so by implementing `FactCheckerFactory` and `FactChecker` interfaces from `@backstage/plugin-tech-insights-common` package. + +To add the default FactChecker into your Tech Insights you need to install the module into your backend application: + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-tech-insights-backend-module-jsonfc +``` + +and modify the `techInsights.ts` file to contain a reference to the FactChecker implementation. + +```diff ++ import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + ++ const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ ++ checks: [], ++ logger, ++ }), + +const builder = new DefaultTechInsightsBuilder({ +logger, +config, +database, +discovery, +factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory +}); +``` + +To be able to run checks, you need to additionally add individual checks into your FactChecker implementation. For examples how to add these, you can check the documentation of the individual implementation of the FactChecker + +#### Modifying check persistence + +The default FactChecker implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows: + +```diff +const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip +const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, ++ checkRegistry: myTechInsightCheckRegistry +}), + +``` diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md new file mode 100644 index 0000000000..fc1bd9217b --- /dev/null +++ b/plugins/tech-insights-backend/api-report.md @@ -0,0 +1,76 @@ +## API Report File for "@backstage/plugin-tech-insights-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Config } from '@backstage/config'; +import express from 'express'; +import { FactChecker } from '@backstage/plugin-tech-insights-common'; +import { FactCheckerFactory } from '@backstage/plugin-tech-insights-common'; +import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-common'; +import { Logger as Logger_2 } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-common'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; + +// @public +export function createRouter< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>(options: RouterOptions): Promise; + +// @public (undocumented) +export class DefaultTechInsightsBuilder< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + constructor(options: TechInsightsOptions); + build(): Promise>; +} + +// @public +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + +// @public +export interface RouterOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + config: Config; + factChecker?: FactChecker; + logger: Logger_2; + persistenceContext: PersistenceContext; +} + +// @public (undocumented) +export type TechInsightsContext< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> = { + factChecker?: FactChecker; + persistenceContext: PersistenceContext; +}; + +// @public (undocumented) +export interface TechInsightsOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + // (undocumented) + config: Config; + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + factCheckerFactory?: FactCheckerFactory; + factRetrievers: FactRetrieverRegistration[]; + // (undocumented) + logger: Logger_2; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js new file mode 100644 index 0000000000..4afe47061f --- /dev/null +++ b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js @@ -0,0 +1,57 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('fact_schemas', table => { + table.comment( + 'The table for tech insight fact schemas. Containing a versioned data model definition for a collection of facts.', + ); + table.increments('id').primary(); + table + .text('ref') + .notNullable() + .comment('Identifier of the fact retriever plugin/package'); + table + .string('version') + .notNullable() + .comment('SemVer string defining the version of schema.'); + table + .text('schema') + .notNullable() + .comment( + 'Fact schema defining the values/types what this version of the fact would contain.', + ); + + table.index('ref', 'fact_schema_ref_idx'); + table.index(['ref', 'version'], 'fact_schema_ref_version_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('fact_schemas', table => { + table.dropIndex([], 'fact_schema_ref_idx'); + table.dropIndex([], 'fact_schema_ref_version_idx'); + }); + await knex.schema.dropTable('fact_schemas'); +}; diff --git a/plugins/tech-insights-backend/migrations/202109061212_facts.js b/plugins/tech-insights-backend/migrations/202109061212_facts.js new file mode 100644 index 0000000000..f80883ef1d --- /dev/null +++ b/plugins/tech-insights-backend/migrations/202109061212_facts.js @@ -0,0 +1,73 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('facts', table => { + table.comment( + 'The table for tech insight fact collections. Contains facts for individual fact retriever namespace/ref.', + ); + table + .bigIncrements('index') + .notNullable() + .comment('An insert counter to ensure ordering'); + table + .text('ref') + .notNullable() + .comment('Unique identifier of the fact retriever plugin/package'); + table + .string('version') + .notNullable() + .comment( + 'SemVer string defining the version of schema this fact is based on.', + ); + table + .dateTime('timestamp') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this entry was created'); + table + .text('entity') + .notNullable() + .comment('Identifier of the entity these facts relate to'); + table + .text('facts') + .notNullable() + .comment( + 'Values of the fact collection stored as key-value pairs in JSON format.', + ); + + table.index('index', 'fact_index_idx'); + table.index('ref', 'fact_ref_idx'); + table.index(['ref', 'entity'], 'fact_ref_entity_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('facts', table => { + table.dropIndex([], 'facts_index_idx'); + table.dropIndex([], 'fact_ref_idx'); + table.dropIndex([], 'fact_ref_entity_idx'); + }); + await knex.schema.dropTable('facts'); +}; diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json new file mode 100644 index 0000000000..fba5daae45 --- /dev/null +++ b/plugins/tech-insights-backend/package.json @@ -0,0 +1,64 @@ +{ + "name": "@backstage/plugin-tech-insights-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "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": "plugins/tech-insights-backend" + }, + "keywords": [ + "backstage", + "tech-insights", + "reporting" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "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.0", + "@backstage/catalog-client": "^0.3.16", + "@backstage/catalog-model": "^0.9.0", + "@backstage/config": "^0.1.8", + "@backstage/errors": "^0.1.1", + "@backstage/plugin-tech-insights-common": "^0.1.0", + "@types/express": "^4.17.6", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "knex": "^0.95.1", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "node-cron": "^3.0.0", + "semver": "^7.3.5", + "uuid": "^8.3.2", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.7.1", + "@types/supertest": "^2.0.8", + "@types/node-cron": "^3.0.0", + "@types/semver": "^7.3.8", + "supertest": "^6.1.3" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/tech-insights-backend/src/index.ts b/plugins/tech-insights-backend/src/index.ts new file mode 100644 index 0000000000..eb54e8cfed --- /dev/null +++ b/plugins/tech-insights-backend/src/index.ts @@ -0,0 +1,26 @@ +/* + * 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 * from './service/router'; +export type { RouterOptions } from './service/router'; + +export { DefaultTechInsightsBuilder } from './service/DefaultTechInsightsBuilder'; +export type { + TechInsightsOptions, + TechInsightsContext, +} from './service/DefaultTechInsightsBuilder'; + +export type { PersistenceContext } from './service/persistence/DatabaseManager'; diff --git a/plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts b/plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts new file mode 100644 index 0000000000..6c5b946c4c --- /dev/null +++ b/plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts @@ -0,0 +1,149 @@ +/* + * 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 { FactRetrieverEngine } from './fact/FactRetrieverEngine'; +import { Logger } from 'winston'; +import { FactRetrieverRegistry } from './fact/FactRetrieverRegistry'; +import { Config } from '@backstage/config'; +import { + PluginDatabaseManager, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { + CheckResult, + FactChecker, + FactCheckerFactory, + FactRetrieverRegistration, + TechInsightCheck, +} from '@backstage/plugin-tech-insights-common'; +import { + DatabaseManager, + PersistenceContext, +} from './persistence/DatabaseManager'; + +/** + * @public + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + * + * Configuration options to initialize TechInsightsBuilder. Generic types params are needed if FactCheckerFactory + * is included for FactChecker creation. + */ +export interface TechInsightsOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * A collection of FactRetrieverRegistrations. + * Used to register FactRetrievers and their schemas and schedule an execution loop for them. + */ + factRetrievers: FactRetrieverRegistration[]; + + /** + * Optional factory exposing a `construct` method to initialize a FactChecker implementation + */ + factCheckerFactory?: FactCheckerFactory; + + logger: Logger; + config: Config; + discovery: PluginEndpointDiscovery; + database: PluginDatabaseManager; +} + +/** + * @public + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + * + * A container for exported implementations related to TechInsights. + * FactChecker is present if an optional FactCheckerFactory is included in the build stage. + */ +export type TechInsightsContext< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> = { + factChecker?: FactChecker; + persistenceContext: PersistenceContext; +}; + +/** + * @public + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + * + * Default implementation of TechInsightsBuilder. + */ +export class DefaultTechInsightsBuilder< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + private readonly options: TechInsightsOptions; + + constructor(options: TechInsightsOptions) { + this.options = options; + } + + /** + * Constructs needed persistence context, fact retriever engine + * and optionally fact checker implementations to be used in the tech insights module. + * + * @returns TechInsightsContext with persistence implementations and optionally an implementation of a FactChecker + */ + async build(): Promise> { + const { + factRetrievers, + factCheckerFactory, + config, + discovery, + database, + logger, + } = this.options; + + const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); + + const persistenceContext = + await DatabaseManager.initializePersistenceContext( + await database.getClient(), + { logger }, + ); + + const factRetrieverEngine = await FactRetrieverEngine.fromConfig({ + repository: persistenceContext.techInsightsStore, + factRetrieverRegistry, + factRetrieverContext: { + config, + discovery, + logger, + }, + }); + + factRetrieverEngine.schedule(); + + if (factCheckerFactory) { + const factChecker = factCheckerFactory.construct( + persistenceContext.techInsightsStore, + ); + return { + persistenceContext, + factChecker, + }; + } + + return { + persistenceContext, + }; + } +} diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts new file mode 100644 index 0000000000..6b9b58e3b8 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { + FactRetriever, + FactRetrieverRegistration, + FactSchema, + TechInsightFact, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-common'; +import { FactRetrieverRegistry } from './FactRetrieverRegistry'; +import { FactRetrieverEngine } from './FactRetrieverEngine'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { schedule } from 'node-cron'; + +jest.mock('node-cron', () => { + const original = jest.requireActual('node-cron'); + return { + ...original, + schedule: jest.fn(), + }; +}); + +const testFactRetriever: FactRetriever = { + ref: 'test-factretriever', + schema: { + version: '0.0.1', + schema: { + testnumberfact: { + type: 'integer', + description: '', + entityKinds: ['component'], + }, + }, + }, + handler: async () => { + return [ + { + ref: 'test-factretriever', + entity: { + namespace: 'a', + kind: 'a', + name: 'a', + }, + facts: { + testnumberfact: 1, + }, + }, + ]; + }, +}; +const cadence = '1 * * * *'; +describe('FactRetrieverEngine', () => { + let engine: FactRetrieverEngine; + let factSchemaAssertionCallback: (ref: string, schema: FactSchema) => void; + let factInsertionAssertionCallback: (facts: TechInsightFact[]) => void; + + const mockRepository: TechInsightsStore = { + insertFacts: (facts: TechInsightFact[]) => { + factInsertionAssertionCallback(facts); + return Promise.resolve(); + }, + insertFactSchema: (ref: string, schema: FactSchema) => { + factSchemaAssertionCallback(ref, schema); + return Promise.resolve(); + }, + } as unknown as TechInsightsStore; + + const mockFactRetrieverRegistry: FactRetrieverRegistry = { + listRetrievers(): FactRetriever[] { + return [testFactRetriever]; + }, + listRegistrations(): FactRetrieverRegistration[] { + return [{ factRetriever: testFactRetriever, cadence }]; + }, + } as unknown as FactRetrieverRegistry; + + const defaultEngineConfig = { + factRetrieverContext: { + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), + discovery: { + getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + }, + }, + factRetrieverRegistry: mockFactRetrieverRegistry, + repository: mockRepository, + }; + + it('Should update fact retriever schemas on initialization', async () => { + factSchemaAssertionCallback = (ref, schema) => { + expect(ref).toEqual('test-factretriever'); + expect(schema).toEqual({ + version: '0.0.1', + schema: { + testnumberfact: { + type: 'integer', + description: '', + entityKinds: ['component'], + }, + }, + }); + }; + + engine = await FactRetrieverEngine.fromConfig(defaultEngineConfig); + }); + it('Should insert facts when scheduled step is run', async () => { + (schedule as jest.Mock).mockImplementation( + (cronCadence: string, retrieverAction: Function) => { + return { + cadence: cronCadence, + triggerScheduledJobNow: retrieverAction, + }; + }, + ); + + factSchemaAssertionCallback = () => {}; + factInsertionAssertionCallback = facts => { + expect(facts).toHaveLength(1); + expect(facts[0]).toEqual({ + ref: 'test-factretriever', + entity: { + namespace: 'a', + kind: 'a', + name: 'a', + }, + facts: { + testnumberfact: 1, + }, + }); + }; + engine = await FactRetrieverEngine.fromConfig(defaultEngineConfig); + engine.schedule(); + const job: any = engine.getJob('test-factretriever'); + job.triggerScheduledJobNow(); + expect(job.cadence!!).toEqual(cadence); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts new file mode 100644 index 0000000000..01e3c12370 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -0,0 +1,135 @@ +/* + * 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 { + FactRetriever, + FactRetrieverContext, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-common'; +import { FactRetrieverRegistry } from './FactRetrieverRegistry'; +import { schedule, validate, ScheduledTask } from 'node-cron'; +import { Logger } from 'winston'; + +function randomDailyCron() { + const rand = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1) + min); + return `${rand(0, 59)} ${rand(0, 23)} * * *`; +} + +function duration(startTimestamp: [number, number]): string { + const delta = process.hrtime(startTimestamp); + const seconds = delta[0] + delta[1] / 1e9; + return `${seconds.toFixed(1)}s`; +} + +export class FactRetrieverEngine { + private scheduledJobs = new Map(); + + constructor( + private readonly repository: TechInsightsStore, + private readonly factRetrieverRegistry: FactRetrieverRegistry, + private readonly factRetrieverContext: FactRetrieverContext, + private readonly logger: Logger, + private readonly defaultCadence?: string, + ) {} + + static async fromConfig({ + repository, + factRetrieverRegistry, + factRetrieverContext, + defaultCadence, + }: { + repository: TechInsightsStore; + factRetrieverRegistry: FactRetrieverRegistry; + factRetrieverContext: FactRetrieverContext; + defaultCadence?: string; + }) { + await Promise.all( + factRetrieverRegistry + .listRetrievers() + .map(it => repository.insertFactSchema(it.ref, it.schema)), + ); + + return new FactRetrieverEngine( + repository, + factRetrieverRegistry, + factRetrieverContext, + factRetrieverContext.logger, + defaultCadence, + ); + } + + schedule() { + const registrations = this.factRetrieverRegistry.listRegistrations(); + const newRegs: string[] = []; + registrations.forEach(registration => { + const { factRetriever, cadence } = registration; + if (!this.scheduledJobs.has(factRetriever.ref)) { + const cronExpression = + cadence || this.defaultCadence || randomDailyCron(); + if (!validate(cronExpression)) { + this.logger.warn( + `Validation failed for cron expression ${cronExpression} when trying to schedule fact retriever ${factRetriever.ref}`, + ); + return; + } + const job = schedule( + cronExpression, + this.createFactRetrieverHandler(factRetriever), + ); + this.scheduledJobs.set(factRetriever.ref, job); + newRegs.push(factRetriever.ref); + } + }); + this.logger.info( + `Scheduled ${newRegs.length} fact retrievers to Fact Retriever Engine.`, + ); + } + + getJob(ref: string) { + return this.scheduledJobs.get(ref); + } + + private createFactRetrieverHandler(factRetriever: FactRetriever) { + return async () => { + const startTimestamp = process.hrtime(); + this.logger.info( + `Retrieving facts for fact retriever ${factRetriever.ref}`, + ); + const facts = await factRetriever.handler(this.factRetrieverContext); + if (this.logger.isDebugEnabled()) { + this.logger.debug( + `Retrieved ${facts.length} facts for fact retriever ${ + factRetriever.ref + } in ${duration(startTimestamp)}`, + ); + } + + try { + await this.repository.insertFacts(factRetriever.ref, facts); + this.logger.info( + `Stored ${facts.length} facts for fact retriever ${ + factRetriever.ref + } in ${duration(startTimestamp)}`, + ); + } catch (e) { + this.logger.warn( + `Failed to insert facts for fact retriever ${factRetriever.ref}`, + e, + ); + } + }; + } +} diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts new file mode 100644 index 0000000000..c76bb6039d --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -0,0 +1,63 @@ +/* + * 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 { + FactRetriever, + FactRetrieverRegistration, + FactSchema, +} from '@backstage/plugin-tech-insights-common'; +import { ConflictError, NotFoundError } from '@backstage/errors'; + +export class FactRetrieverRegistry { + private readonly retrievers = new Map(); + + constructor(retrievers: FactRetrieverRegistration[]) { + retrievers.forEach(it => { + this.register(it); + }); + } + + register(registration: FactRetrieverRegistration) { + if (this.retrievers.has(registration.factRetriever.ref)) { + throw new ConflictError( + `Tech insight fact retriever with reference '${registration.factRetriever.ref}' has already been registered`, + ); + } + this.retrievers.set(registration.factRetriever.ref, registration); + } + + get(retrieverReference: string): FactRetriever { + const registration = this.retrievers.get(retrieverReference); + if (!registration) { + throw new NotFoundError( + `Tech insight fact retriever with reference '${retrieverReference}' is not registered.`, + ); + } + return registration.factRetriever; + } + + listRetrievers(): FactRetriever[] { + return [...this.retrievers.values()].map(it => it.factRetriever); + } + + listRegistrations(): FactRetrieverRegistration[] { + return [...this.retrievers.values()]; + } + + getSchemas(): FactSchema[] { + return this.listRetrievers().map(it => it.schema); + } +} diff --git a/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts b/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts new file mode 100644 index 0000000000..75fb313f80 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts @@ -0,0 +1,99 @@ +/* + * 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, resolvePackagePath } from '@backstage/backend-common'; +import knexFactory, { Knex } from 'knex'; +import { Logger } from 'winston'; +import { v4 as uuidv4 } from 'uuid'; +import { TechInsightsDatabase } from './TechInsightsDatabase'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-tech-insights-backend', + 'migrations', +); + +/** + * A Container for persistence related components in TechInsights + * + * @public + */ +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + +export type CreateDatabaseOptions = { + logger: Logger; +}; + +const defaultOptions: CreateDatabaseOptions = { + logger: getVoidLogger(), +}; + +/** + * A factory class to construct persistence context for both running implmentation and test cases. + * + * @public + */ +export class DatabaseManager { + public static async initializePersistenceContext( + knex: Knex, + options: CreateDatabaseOptions = defaultOptions, + ): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return { + techInsightsStore: new TechInsightsDatabase(knex, options.logger), + }; + } + + public static async createTestDatabase( + knex: Knex, + ): Promise { + const knexInstance = knex ?? (await this.createTestDatabaseConnection()); + return await this.initializePersistenceContext(knexInstance); + } + + public static async createTestDatabaseConnection(): Promise { + const config: Knex.Config = { + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }; + + let knexInstance = knexFactory(config); + if (typeof config.connection !== 'string') { + const tempDbName = `d${uuidv4().replace(/-/g, '')}`; + await knexInstance.raw(`CREATE DATABASE ${tempDbName};`); + knexInstance = knexFactory({ + ...config, + connection: { + ...config.connection, + database: tempDbName, + }, + }); + } + + knexInstance.client.pool.on( + 'createSuccess', + (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }, + ); + + return knexInstance; + } +} diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts new file mode 100644 index 0000000000..ab54cf819d --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts @@ -0,0 +1,218 @@ +/* + * 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 } from './DatabaseManager'; +import { DateTime, Duration } from 'luxon'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { Knex } from 'knex'; + +const factSchemas = [ + { + ref: 'test-schema', + version: '0.0.1-test', + schema: JSON.stringify({ + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + entityKinds: ['component'], + }, + }), + }, +]; +const additionalFactSchemas = [ + { + ref: 'test-schema', + version: '1.2.1-test', + schema: JSON.stringify({ + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + entityKinds: ['component'], + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + entityKinds: ['service'], + }, + }), + }, + { + ref: 'test-schema', + version: '1.1.1-test', + schema: JSON.stringify({ + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + entityKinds: ['service'], + }, + }), + }, +]; + +const now = DateTime.now().toISO(); +const shortlyInTheFuture = DateTime.now() + .plus(Duration.fromMillis(555)) + .toISO(); +const farInTheFuture = DateTime.now() + .plus(Duration.fromMillis(555666777)) + .toISO(); + +const facts = [ + { + timestamp: now, + ref: 'test-fact', + version: '0.0.1-test', + entity: 'a/a/a', + facts: JSON.stringify({ + testNumberFact: 1, + }), + }, + { + timestamp: shortlyInTheFuture, + ref: 'test-fact', + version: '0.0.1-test', + entity: 'a/a/a', + facts: JSON.stringify({ + testNumberFact: 2, + }), + }, +]; + +const additionalFacts = [ + { + timestamp: farInTheFuture, + ref: 'test-fact', + version: '0.0.1-test', + entity: 'a/a/a', + facts: JSON.stringify({ + testNumberFact: 3, + }), + }, +]; + +describe('Tech Insights database', () => { + let store: TechInsightsStore; + let testDbClient: Knex; + beforeAll(async () => { + testDbClient = await DatabaseManager.createTestDatabaseConnection(); + store = (await DatabaseManager.createTestDatabase(testDbClient)) + .techInsightsStore; + await testDbClient.batchInsert('fact_schemas', factSchemas); + await testDbClient.batchInsert('facts', facts); + }); + + const baseAssertionFact = { + ref: 'test-fact', + entity: { namespace: 'a', kind: 'a', name: 'a' }, + timestamp: DateTime.fromISO(shortlyInTheFuture), + version: '0.0.1-test', + facts: { testNumberFact: 2 }, + }; + + it('should be able to return latest schema', async () => { + const schemas = await store.getLatestSchemas(); + expect(schemas[0]).toMatchObject({ + ref: 'test-schema', + version: '0.0.1-test', + schema: { + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + entityKinds: ['component'], + }, + }, + }); + }); + + it('should return last schema based on semver', async () => { + await testDbClient.batchInsert('fact_schemas', additionalFactSchemas); + + const schemas = await store.getLatestSchemas(); + expect(schemas[0]).toMatchObject({ + ref: 'test-schema', + version: '1.2.1-test', + schema: { + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + entityKinds: ['component'], + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + entityKinds: ['service'], + }, + }, + }); + }); + + it('should return latest facts only for the correct ref', async () => { + const returnedFact = await store.getLatestFactsForRefs( + ['test-fact'], + 'a/a/a', + ); + expect(returnedFact['test-fact']).toMatchObject(baseAssertionFact); + }); + + it('should return latest facts for multiple refs', async () => { + await testDbClient.batchInsert( + 'facts', + additionalFacts.map(fact => ({ + ...fact, + ref: 'second-test-fact', + timestamp: farInTheFuture, + })), + ); + const returnedFacts = await store.getLatestFactsForRefs( + ['test-fact', 'second-test-fact'], + 'a/a/a', + ); + + expect(returnedFacts['test-fact']).toMatchObject({ + ...baseAssertionFact, + }); + expect(returnedFacts['second-test-fact']).toMatchObject({ + ...baseAssertionFact, + ref: 'second-test-fact', + timestamp: DateTime.fromISO(farInTheFuture), + facts: { testNumberFact: 3 }, + }); + }); + + it('should return facts correctly between time range', async () => { + await testDbClient.batchInsert('facts', additionalFacts); + const returnedFacts = await store.getFactsBetweenTimestampsForRefs( + ['test-fact'], + 'a/a/a', + DateTime.fromISO(now), + DateTime.fromISO(shortlyInTheFuture).plus(Duration.fromMillis(10)), + ); + expect(returnedFacts['test-fact']).toHaveLength(2); + + expect(returnedFacts['test-fact'][0]).toMatchObject({ + ...baseAssertionFact, + timestamp: DateTime.fromISO(now), + facts: { testNumberFact: 1 }, + }); + expect(returnedFacts['test-fact'][1]).toMatchObject({ + ...baseAssertionFact, + }); + expect(returnedFacts['test-fact']).not.toContainEqual({ + ...baseAssertionFact, + timestamp: DateTime.fromISO(farInTheFuture), + facts: { testNumberFact: 3 }, + }); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts new file mode 100644 index 0000000000..1bdf18249f --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -0,0 +1,188 @@ +/* + * 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 { + FactSchema, + TechInsightFact, + FlatTechInsightFact, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-common'; +import { rsort } from 'semver'; +import { groupBy } from 'lodash'; +import { DateTime } from 'luxon'; +import { Logger } from 'winston'; + +export type RawDbFactRow = { + ref: string; + version: string; + timestamp: Date | string; + entity: string; + facts: string; +}; + +type RawDbFactSchemaRow = { + id: number; + ref: string; + version: string; + schema: string; +}; + +export class TechInsightsDatabase implements TechInsightsStore { + private readonly CHUNK_SIZE = 50; + + constructor(private readonly db: Knex, private readonly logger: Logger) {} + + async getLatestSchemas(refs?: string[]): Promise { + const queryBuilder = this.db('fact_schemas'); + if (refs) { + queryBuilder.whereIn('ref', refs); + } + const existingSchemas = await queryBuilder.orderBy('id', 'desc').select(); + + const groupedSchemas = groupBy(existingSchemas, 'ref'); + return Object.values(groupedSchemas) + .map(schemas => { + const sorted = rsort(schemas.map(it => it.version)); + return schemas.find(it => it.version === sorted[0])!!; + }) + .map((it: RawDbFactSchemaRow) => ({ + ...it, + schema: JSON.parse(it.schema), + })); + } + + async insertFactSchema(ref: string, schema: FactSchema) { + const existingSchemas = await this.db('fact_schemas') + .where({ ref }) + .select(); + const exists = existingSchemas.some( + it => it.ref === ref && it.version === schema.version, + ); + + if (!exists) { + await this.db('fact_schemas').insert({ + ref, + version: schema.version, + schema: JSON.stringify(schema.schema), + }); + } + } + + async insertFacts(ref: string, facts: TechInsightFact[]): Promise { + if (facts.length === 0) return; + const currentSchema = await this.getLatestSchema(ref); + const factRows = facts.map(it => { + const { namespace, name, kind } = it.entity; + return { + ref: ref, + version: currentSchema.version, + entity: `${namespace}/${kind}/${name}`.toLocaleLowerCase('en-US'), + facts: JSON.stringify(it.facts), + ...(it.timestamp && { timestamp: it.timestamp.toJSDate() }), + }; + }); + await this.db.transaction(async tx => { + await tx.batchInsert('facts', factRows, this.CHUNK_SIZE); + }); + } + + async getLatestFactsForRefs( + refs: string[], + entityTriplet: string, + ): Promise<{ [p: string]: FlatTechInsightFact }> { + const results = await this.db('facts') + .where({ entity: entityTriplet }) + .and.whereIn('ref', refs) + .join( + this.db('facts') + .max('timestamp') + .column('ref as subRef') + .groupBy('ref') + .as('subQ'), + 'facts.ref', + 'subQ.subRef', + ); + return this.dbFactRowsToTechInsightFacts(results); + } + + async getFactsBetweenTimestampsForRefs( + refs: string[], + entityTriplet: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ + [p: string]: FlatTechInsightFact[]; + }> { + const results = await this.db('facts') + .where({ entity: entityTriplet }) + .and.whereIn('ref', refs) + .and.whereBetween('timestamp', [ + startDateTime.toISO(), + endDateTime.toISO(), + ]); + + return groupBy( + results.map(it => { + const [namespace, kind, name] = it.entity.split('/'); + const timestamp = + typeof it.timestamp === 'string' + ? DateTime.fromISO(it.timestamp) + : DateTime.fromJSDate(it.timestamp); + return { + ref: it.ref, + entity: { namespace, kind, name }, + timestamp, + version: it.version, + facts: JSON.parse(it.facts), + }; + }), + 'ref', + ); + } + + private async getLatestSchema(ref: string): Promise { + const existingSchemas = await this.db('fact_schemas') + .where({ ref }) + .orderBy('id', 'desc') + .select(); + if (existingSchemas.length < 1) { + this.logger.warn(`No schema found for ${ref}. `); + throw new Error(`No schema found for ${ref}. `); + } + const sorted = rsort(existingSchemas.map(it => it.version)); + return existingSchemas.find(it => it.version === sorted[0])!!; + } + + private dbFactRowsToTechInsightFacts(rows: RawDbFactRow[]) { + return rows.reduce((acc, it) => { + const [namespace, kind, name] = it.entity.split('/'); + const timestamp = + typeof it.timestamp === 'string' + ? DateTime.fromISO(it.timestamp) + : DateTime.fromJSDate(it.timestamp); + return { + ...acc, + [it.ref]: { + ref: it.ref, + entity: { namespace, kind, name }, + timestamp, + version: it.version, + facts: JSON.parse(it.facts), + }, + }; + }, {}); + } +} diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts new file mode 100644 index 0000000000..1433722e6c --- /dev/null +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { DefaultTechInsightsBuilder } from './DefaultTechInsightsBuilder'; +import { createRouter } from './router'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import request from 'supertest'; +import express from 'express'; +import { PersistenceContext } from './persistence/DatabaseManager'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { DateTime } from 'luxon'; +import { Knex } from 'knex'; + +describe('Tech Insights router tests', () => { + let app: express.Express; + + const latestFactsForRefsMock = jest.fn(); + const factsBetweenTimestampsForRefsMock = jest.fn(); + const latestSchemasMock = jest.fn(); + + const mockPersistenceContext: PersistenceContext = { + techInsightsStore: { + getLatestFactsForRefs: latestFactsForRefsMock, + getFactsBetweenTimestampsForRefs: factsBetweenTimestampsForRefsMock, + getLatestSchemas: latestSchemasMock, + } as unknown as TechInsightsStore, + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + beforeAll(async () => { + const techInsightsContext = await new DefaultTechInsightsBuilder({ + database: { + getClient: () => { + return Promise.resolve({ + migrate: { + latest: () => {}, + }, + }) as unknown as Promise; + }, + }, + logger: getVoidLogger(), + factRetrievers: [], + config: ConfigReader.fromConfigs([]), + discovery: { + getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + }, + }).build(); + + const router = await createRouter({ + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), + ...techInsightsContext, + persistenceContext: mockPersistenceContext, + }); + + app = express().use(router); + }); + + it('should be able to retrieve latest schemas', async () => { + await request(app).get('/fact-schemas').expect(200); + expect(latestSchemasMock).toHaveBeenCalled(); + }); + + it('should not contain check endpoints when checker not present', async () => { + await request(app).get('/checks').expect(404); + await request(app).get('/checks/a/a/a').expect(404); + }); + + it('should parse be able to parse ref request params for fact retrieval', async () => { + await request(app) + .get('/facts/latest/a/a/a') + .query({ refs: ['firstref', 'secondref'] }) + .expect(200); + expect(latestFactsForRefsMock).toHaveBeenCalledWith( + ['firstref', 'secondref'], + 'a/a/a', + ); + }); + + it('should parse be able to parse datetime request params for fact retrieval', async () => { + await request(app) + .get('/facts/range/a/a/a') + .query({ + refs: ['firstref', 'secondref'], + startDatetime: '2021-12-12T12:12:12', + endDatetime: '2022-11-11T11:11:11', + }) + .expect(200); + expect(factsBetweenTimestampsForRefsMock).toHaveBeenCalledWith( + ['firstref', 'secondref'], + 'a/a/a', + DateTime.fromISO('2021-12-12T12:12:12.000+00:00'), + DateTime.fromISO('2022-11-11T11:11:11.000+00:00'), + ); + }); + + it('should respond gracefully on parsing errors', async () => { + await request(app) + .get('/facts/range/a/a/a') + .query({ + refs: ['firstref', 'secondref'], + startDatetime: '2021-12-1222T12:12:12', + endDatetime: '2022-1122-11T11:11:11', + }) + .expect(422); + expect(latestFactsForRefsMock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts new file mode 100644 index 0000000000..3649f522dd --- /dev/null +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -0,0 +1,136 @@ +/* + * 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 express from 'express'; +import Router from 'express-promise-router'; +import { Config } from '@backstage/config'; +import { + FactChecker, + TechInsightCheck, + CheckResult, +} from '@backstage/plugin-tech-insights-common'; +import { Logger } from 'winston'; +import { DateTime } from 'luxon'; +import { PersistenceContext } from './persistence/DatabaseManager'; + +/** + * @public + * + * RouterOptions to construct TechInsights endpoints + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + */ +export interface RouterOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * Optional FactChecker implementation. If omitted, endpoints are not constructed + */ + factChecker?: FactChecker; + + /** + * TechInsights PersistenceContext. Should contain an implementation of TechInsightsStore + */ + persistenceContext: PersistenceContext; + + /** + * Backstage config object + */ + config: Config; + + /** + * Implementation of Winston logger + */ + logger: Logger; +} + +/** + * @public + * + * Constructs a tech-insights router. + * + * Exposes endpoints to handle facts + * Exposes optional endpoints to handle checks if a FactChecker implementation is passed in + * + * @param options - RouterOptions object + */ +export async function createRouter< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>(options: RouterOptions): Promise { + const router = Router(); + router.use(express.json()); + const { persistenceContext, factChecker, logger } = options; + const { techInsightsStore } = persistenceContext; + + if (factChecker) { + logger.info('Fact checker configured. Enabling fact checking endpoints.'); + router.get('/checks', async (_req, res) => { + return res.send(await factChecker.getChecks()); + }); + + router.get('/checks/:namespace/:kind/:name', async (req, res) => { + const { namespace, kind, name } = req.params; + const checks = req.query.checks as string[]; + const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; + try { + const checkResult = await factChecker.runChecks(entityTriplet, checks); + return res.send(checkResult); + } catch (e) { + return res.status(500).json({ message: e.message }).send(); + } + }); + } else { + logger.info( + 'Starting tech insights module without fact checking endpoints.', + ); + } + + router.get('/fact-schemas', async (req, res) => { + const refs = req.query.refs as string[]; + return res.send(await techInsightsStore.getLatestSchemas(refs)); + }); + + router.get('/facts/latest/:namespace/:kind/:name', async (req, res) => { + const { namespace, kind, name } = req.params; + const refs = req.query.refs as string[]; + const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; + return res.send( + await techInsightsStore.getLatestFactsForRefs(refs, entityTriplet), + ); + }); + + router.get('/facts/range/:namespace/:kind/:name', async (req, res) => { + const { namespace, kind, name } = req.params; + const refs = req.query.refs as string[]; + const startDatetime = DateTime.fromISO(req.query.startDatetime as string); + const endDatetime = DateTime.fromISO(req.query.endDatetime as string); + if (!startDatetime.isValid || !endDatetime.isValid) { + return res.status(422).send('Failed to parse datetime from request'); + } + const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; + return res.send( + await techInsightsStore.getFactsBetweenTimestampsForRefs( + refs, + entityTriplet, + startDatetime, + endDatetime, + ), + ); + }); + return router; +} diff --git a/plugins/tech-insights-backend/src/setupTests.ts b/plugins/tech-insights-backend/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/tech-insights-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 {}; diff --git a/plugins/tech-insights-common/.eslintrc.js b/plugins/tech-insights-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/tech-insights-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/tech-insights-common/CHANGELOG.md b/plugins/tech-insights-common/CHANGELOG.md new file mode 100644 index 0000000000..17e0211ab0 --- /dev/null +++ b/plugins/tech-insights-common/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-tech-insights-backend + +## 0.0.1 + +### Patch Changes + +- Initial implementation diff --git a/plugins/tech-insights-common/README.md b/plugins/tech-insights-common/README.md new file mode 100644 index 0000000000..651c0e969d --- /dev/null +++ b/plugins/tech-insights-common/README.md @@ -0,0 +1,3 @@ +# Tech Insights Common + +Common types and functionalities for tech insights, to be shared between tech-insights-backend and individual implementations of FactRetrievers, FactCheckers and their persistence options. diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md new file mode 100644 index 0000000000..6d0d40239b --- /dev/null +++ b/plugins/tech-insights-common/api-report.md @@ -0,0 +1,167 @@ +## API Report File for "@backstage/plugin-tech-insights-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { DateTime } from 'luxon'; +import { Logger as Logger_2 } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public +export interface BooleanCheckResult extends CheckResult { + // (undocumented) + result: boolean; +} + +// @public +export interface CheckResponse { + description: string; + factRefs: string[]; + id: string; + metadata?: Record; + name: string; +} + +// @public +export type CheckResult = { + facts: FactResponse; + check: CheckResponse; +}; + +// @public +export interface FactChecker< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + addCheck(check: CheckType): Promise; + getChecks(): Promise; + runChecks(entity: string, checks: string[]): Promise; + validate(check: CheckType): Promise; +} + +// @public +export interface FactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + // (undocumented) + construct( + repository: TechInsightsStore, + ): FactChecker; +} + +// @public +export type FactResponse = { + [key: string]: { + ref: string; + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + description: string; + value: number | string | boolean | DateTime | []; + since?: string; + metadata?: Record; + entityKinds: string[]; + }; +}; + +// @public +export interface FactRetriever { + handler: (ctx: FactRetrieverContext) => Promise; + ref: string; + schema: FactSchema; +} + +// @public +export type FactRetrieverContext = { + config: Config; + discovery: PluginEndpointDiscovery; + logger: Logger_2; +}; + +// @public +export type FactRetrieverRegistration = { + factRetriever: FactRetriever; + cadence?: string; +}; + +// @public +export type FactSchema = { + version: string; + schema: FactValueDefinitions; +}; + +// @public +export type FactValueDefinitions = { + [key: string]: { + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + description: string; + since?: string; + metadata?: Record; + entityKinds: string[]; + }; +}; + +// @public +export type FlatTechInsightFact = TechInsightFact & { + ref: string; +}; + +// @public +export interface TechInsightCheck { + // (undocumented) + description: string; + factRefs: string[]; + failureMetadata?: Record; + id: string; + // (undocumented) + name: string; + successMetadata?: Record; +} + +// @public +export interface TechInsightCheckRegistry { + // (undocumented) + get(checkId: string): Promise; + // (undocumented) + getAll(checks: string[]): Promise; + // (undocumented) + list(): Promise; + // (undocumented) + register(check: CheckType): Promise; +} + +// @public +export type TechInsightFact = { + entity: { + namespace: string; + kind: string; + name: string; + }; + facts: Record; + timestamp?: DateTime; +}; + +// @public +export interface TechInsightsStore { + getFactsBetweenTimestampsForRefs( + refs: string[], + entity: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ + [factRef: string]: FlatTechInsightFact[]; + }>; + // (undocumented) + getLatestFactsForRefs( + refs: string[], + entity: string, + ): Promise<{ + [factRef: string]: FlatTechInsightFact; + }>; + getLatestSchemas(refs?: string[]): Promise; + insertFacts(ref: string, facts: TechInsightFact[]): Promise; + insertFactSchema(ref: string, schema: FactSchema): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json new file mode 100644 index 0000000000..37d66a9379 --- /dev/null +++ b/plugins/tech-insights-common/package.json @@ -0,0 +1,45 @@ +{ + "name": "@backstage/plugin-tech-insights-common", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "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": "plugins/tech-insights-common" + }, + "keywords": [ + "backstage", + "tech-insights" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "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.0", + "@backstage/config": "^0.1.8", + "@types/luxon": "^2.0.5", + "luxon": "^2.0.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.7.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights-common/src/checks.ts b/plugins/tech-insights-common/src/checks.ts new file mode 100644 index 0000000000..eaa4792fe3 --- /dev/null +++ b/plugins/tech-insights-common/src/checks.ts @@ -0,0 +1,159 @@ +/* + * 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 { TechInsightsStore } from './persistence'; +import { CheckResponse, FactResponse } from './responses'; + +/** + * A factory wrapper to construct FactChecker implementations. + * + * @public + * @typeParam CheckType - Implementation specific Check. Can extend TechInsightCheck with additional information + * @typeParam CheckResultType - Implementation specific result of a check. Can extend CheckResult with additional information + */ +export interface FactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * @param repository - TechInsightsStore + * @returns an implementation of a FactChecker for generic types defined in the factory + */ + construct( + repository: TechInsightsStore, + ): FactChecker; +} + +/** + * FactChecker interface + * + * A generic interface that can be implemented to create checkers for specific check and check return types. + * This is used especially when creating Scorecards and displaying results of rules when run against facts. + * + * @public + * @typeParam CheckType - Implementation specific Check. Can extend TechInsightCheck with additional information + * @typeParam CheckResultType - Implementation specific result of a check. Can extend CheckResult with additional information + */ +export interface FactChecker< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * Runs checks against an entity. + * + * @param entity - A reference to an entity to run checks against. In a format namespace/kind/name + * @param checks - A collection of checks to run against provided entity + * @returns - A collection containing check/fact information and the actual results of the check + */ + runChecks(entity: string, checks: string[]): Promise; + + /** + * Adds and stores new checks so they can be run checks against. + * Implementation should ideally run validation against the check. + * + * @param check - The actual check to be added. + * @returns - An indicator if fact was successfully added + */ + addCheck(check: CheckType): Promise; + + /** + * Retrieves all available checks that can be used to run checks against. + * The implementation can be just a piping through to CheckRegistry implementation if such is in use. + * + * @returns - A collection of checks + */ + getChecks(): Promise; + + /** + * Validates if check is valid and can be run with the current implementation + * + * @param check - The check to be validated + * @returns - Validation result + */ + validate(check: CheckType): Promise; +} + +/** + * Registry containing checks for tech insights. + * + * @public + * @typeParam CheckType - Implementation specific Check. Can extend TechInsightCheck with additional information + * + */ +export interface TechInsightCheckRegistry { + register(check: CheckType): Promise; + get(checkId: string): Promise; + getAll(checks: string[]): Promise; + list(): Promise; +} + +/** + * Generic CheckResult + * + * Contains information about the facts used to calculate the check result + * and information about the check itself. Both may include metadata to be able to display additional information. + * A collection of these should be parseable by the frontend to display scorecards + * + * @public + */ +export type CheckResult = { + facts: FactResponse; + check: CheckResponse; +}; + +/** + * CheckResult of type Boolean. + * + * @public + */ +export interface BooleanCheckResult extends CheckResult { + result: boolean; +} + +/** + * Generic definition of a check for Tech Insights + * + * @public + */ +export interface TechInsightCheck { + /** + * Unique identifier of the check + * + * Used to identify which checks to use when running checks. + */ + id: string; + + name: string; + description: string; + + /** + * A collection of string referencing fact rows that a check will be run against. + * + * References the fact container, aka fact retriever itself which may or may not contain multiple individual facts and values + */ + factRefs: string[]; + + /** + * Metadata to be returned in case a check has been successfully evaluated + * Can contain links, description texts or other actionable items + */ + successMetadata?: Record; + + /** + * Metadata to be returned in case a check evaluation has ended in failure + * Can contain links, description texts or other actionable items + */ + failureMetadata?: Record; +} diff --git a/plugins/tech-insights-common/src/facts.ts b/plugins/tech-insights-common/src/facts.ts new file mode 100644 index 0000000000..1ae507adf8 --- /dev/null +++ b/plugins/tech-insights-common/src/facts.ts @@ -0,0 +1,196 @@ +/* + * 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 { DateTime } from 'luxon'; +import { Config } from '@backstage/config'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Logger } from 'winston'; + +/** + * A container for facts. The shape of the fact records needs to correspond to the FactSchema with same `ref` value. + * Each container contains a reference to an entity which can be a Backstage entity or a generic construct + * outside of backstage with same shape. + * + * Container may contain multiple individual facts and their values + * + * @public + */ +export type TechInsightFact = { + /** + * Entity reference that this fact relates to + */ + entity: { + namespace: string; + kind: string; + name: string; + }; + + /** + * A collection of fact values as key value pairs. + * + * Key indicates fact name as it is defined in FactSchema + */ + facts: Record; + + /** + * Optional timestamp value which can be used to override retrieval time of the fact row. + * Otherwise when stored into data storage, defaults to current time + */ + timestamp?: DateTime; +}; + +/** + + * Response type used when returning from database and API. + * Adds a field for ref for easier usage + * + * @public + */ +export type FlatTechInsightFact = TechInsightFact & { + /** + * Reference and unique identifier of the fact row + */ + ref: string; +}; + +/** + * @public + * + * A record type to specify individual fact shapes + * + * Used as part of a schema to validate, identify and generically construct usage implementations + * of individual fact values in the system. + */ +export type FactValueDefinitions = { + [key: string]: { + /** + * Type of the individual fact value + * + * Numbers are split into integers and floating point values. + * `set` indicates a collection of values + */ + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + + /** + * A description of this individual fact value + */ + description: string; + + /** + * Optional semver string to indicate when this specific fact definition was added to the schema + */ + since?: string; + + /** + * Metadata related to an individual fact. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + * + * examples: + * ``` + * \{ + * link: 'https://sonarqube.mycompany.com/fix-these-issues', + * suggestion: 'To affect this value, you can do x, y, z', + * minValue: 0 + * \} + * ``` + */ + metadata?: Record; + + /** + * A list of entity kind descriptors to indicate if this fact is valid for an entity kind + */ + entityKinds: string[]; + }; +}; + +/** + * @public + * + * Container for FactSchema + */ +export type FactSchema = { + /** + * Semver string indicating the version of this schema + */ + version: string; + /** + * Actual schema definitions for this schema + */ + schema: FactValueDefinitions; +}; + +/** + * @public + * + * FactRetrieverContext injected into individual handler methods of FactRetriever implementations. + * The context can be used to construct logic to retrieve entities, contact integration points + * and fetch and calculate fact values from external sources. + */ +export type FactRetrieverContext = { + config: Config; + discovery: PluginEndpointDiscovery; + logger: Logger; +}; + +/** + * @public + * + * FactRetriever interface + * + * A component specifying + */ +export interface FactRetriever { + /** + * A unique identifier of the retriever. + * Used to identify and store individual facts returned from this retriever + * and schemas defined by this retriever. + */ + ref: string; + + /** + * Handler function that needs to be implemented to retrieve fact values for entities. + * + * @param ctx - FactRetrieverContext which can be used to retrieve config and contact integrations + * @returns - A collection of TechInsightFacts grouped by entities. + */ + handler: (ctx: FactRetrieverContext) => Promise; + + /** + * A fact schema defining the shape of data returned from the handler method for each entity + */ + schema: FactSchema; +} + +/** + * @public + * + * Registration of a fact retriever + * Used to add and schedule individual fact retrievers to the fact retriever engine. + */ +export type FactRetrieverRegistration = { + /** + * Actual FactRetriever implementation + */ + factRetriever: FactRetriever; + + /** + * Cron expression to indicate when the retriever should be triggered. + * Defaults to a random point in time every 24h + * + */ + cadence?: string; +}; diff --git a/plugins/tech-insights-common/src/index.test.ts b/plugins/tech-insights-common/src/index.test.ts new file mode 100644 index 0000000000..0c54118f9a --- /dev/null +++ b/plugins/tech-insights-common/src/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as anything from './'; + +describe('tech-insights-common', () => { + // Types only + it('should exist', () => { + expect(anything).toBeTruthy(); + }); +}); diff --git a/plugins/tech-insights-common/src/index.ts b/plugins/tech-insights-common/src/index.ts new file mode 100644 index 0000000000..982ae0ac41 --- /dev/null +++ b/plugins/tech-insights-common/src/index.ts @@ -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 * from './checks'; +export * from './facts'; +export * from './persistence'; +export * from './responses'; diff --git a/plugins/tech-insights-common/src/persistence.ts b/plugins/tech-insights-common/src/persistence.ts new file mode 100644 index 0000000000..9ffb6a9a28 --- /dev/null +++ b/plugins/tech-insights-common/src/persistence.ts @@ -0,0 +1,79 @@ +/* + * 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 { FactSchema, TechInsightFact, FlatTechInsightFact } from './facts'; +import { DateTime } from 'luxon'; + +/** + * TechInsights Database + * + * @public + */ +export interface TechInsightsStore { + /** + * Stores fact containers as rows into data store. + * Individual items in array correspond to a fact schema based on reference and entity based on entity identifier. + * + * Each row may contain multiple individual facts and values + * + * @param ref - Unique identifier of the fact retriever these facts relate to + * @param facts - A collection of TechInsightFacts + */ + insertFacts(ref: string, facts: TechInsightFact[]): Promise; + + /** + * @param refs - A collection of reference string to a fact row + * @param entity - A string identifying an entity. In a format namespace/kind/name + * + * @returns - An object keyed by a fact reference and containing an individual TechInsightFact + */ + getLatestFactsForRefs( + refs: string[], + entity: string, + ): Promise<{ [factRef: string]: FlatTechInsightFact }>; + + /** + * Retrieves fact values identified by fact row references for an individual entity. + * + * @param refs - A collection of reference string to a fact row + * @param entity - A string identifying an entity. In a format namespace/kind/name + * @param startDateTime - DateTime object indicating start of the time frame + * @param endDateTime - DateTime object indicating start of the time frame + * + * @returns - An object keyed by a fact reference and containing a collection of TechInsightFacts matching the time frame + */ + getFactsBetweenTimestampsForRefs( + refs: string[], + entity: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ [factRef: string]: FlatTechInsightFact[] }>; + + /** + * Stores versioned fact schemas into data store + * + * @param ref - Identifier of the fact schema. Reference to a fact retriever. + * @param schema - The actual schema to store + */ + insertFactSchema(ref: string, schema: FactSchema): Promise; + + /** + * Retrieves latest versions (as defined by semver) of fact schemas from the data store. + * + * @param refs - Collection of refs to return. If omitted, all Schemas should be returned. + * @returns - A collection of schemas + */ + getLatestSchemas(refs?: string[]): Promise; +} diff --git a/plugins/tech-insights-common/src/responses.ts b/plugins/tech-insights-common/src/responses.ts new file mode 100644 index 0000000000..983a68ee2f --- /dev/null +++ b/plugins/tech-insights-common/src/responses.ts @@ -0,0 +1,100 @@ +/* + * 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 { DateTime } from 'luxon'; + +/** + * @public + * + * Response type for checks. + */ +export interface CheckResponse { + /** + * Identifier of the Check + */ + id: string; + /** + * Human readable name of the Check + */ + name: string; + /** + * Description of the Check + */ + description: string; + + /** + * A collection of references to fact rows used to run this checks against + */ + factRefs: string[]; + + /** + * Metadata related to a check. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + */ + metadata?: Record; +} + +/** + * @public + * + * Individual fact response type. + * Keyed by the name of the fact + */ +export type FactResponse = { + [key: string]: { + /** + * Reference and unique identifier of the fact row + */ + ref: string; + /** + * Type of the individual fact value + * + * Numbers are split into integers and floating point values. + * `set` indicates a collection of values + */ + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + + /** + * Description of the individual fact + */ + description: string; + + /** + * Actual value of the fact + */ + value: number | string | boolean | DateTime | []; + + /** + * An optional SemVer version identifying when this fact was added to the FactSchema + */ + since?: string; + + /** + * Metadata related to an individual fact. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + */ + metadata?: Record; + + /** + * A list of entity kind descriptors to indicate if this fact is valid for an entity kind + */ + entityKinds: string[]; + }; +}; diff --git a/plugins/tech-insights-common/src/setupTests.ts b/plugins/tech-insights-common/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/tech-insights-common/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 {}; diff --git a/yarn.lock b/yarn.lock index c6168a30cf..64f9d5d995 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2172,7 +2172,7 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" -"@backstage/catalog-client@^0.3.18": +"@backstage/catalog-client@^0.3.16", "@backstage/catalog-client@^0.3.18": version "0.3.19" resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.3.19.tgz#109b0fde1cc2d9a306635f3775fc8f6174172b14" integrity sha512-0ydcC/1ISNgR8SggOUnMNNHtsRwrKN5GX+AXgTVdZk4qpz1MqG1+fzrNld9V7KVvXdsbGDZAl/b5a7/FJEpCqg== @@ -7228,6 +7228,11 @@ resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.4.tgz#f7b5a86ccd843c0ccaddfaedd9ee1081bc1cde3b" integrity sha512-l3xuhmyF2kBldy15SeY6d6HbK2BacEcSK1qTF1ISPtPHr29JH0C1fndz9ExXLKpGl0J6pZi+dGp1i5xesMt60Q== +"@types/luxon@^2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.5.tgz#29d3b095d55ee50df8f4cf109b16009334d9828e" + integrity sha512-GKrG5v16BOs9XGpouu33hOkAFaiSDi3ZaDXG9F2yAoyzHRBtksZnI60VWY5aM/yAENCccBejrxw8jDY+9OVlxw== + "@types/markdown-to-jsx@^6.11.3": version "6.11.3" resolved "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz#cdd1619308fecbc8be7e6a26f3751260249b020e" @@ -7300,6 +7305,18 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-cron@^2.0.4": + version "2.0.5" + resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-2.0.5.tgz#e244709a86d32453c5a702ced35b53db683fbc8e" + integrity sha512-rQ4kduTmgW11tbtx0/RsoybYHHPu4Vxw5v5ZS5qUKNerlEAI8r8P1F5UUZ2o2HTvzG759sbFxuRuqWxU8zc+EQ== + dependencies: + "@types/tz-offset" "*" + +"@types/node-cron@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.0.tgz#f946cefb5c05c64f460090f6be97bd50460c8898" + integrity sha512-RNBIyVwa/1v2r8/SqK8tadH2sJlFRAo5Ghac/cOcCv4Kp94m0I03UmAh9WVhCqS9ZdB84dF3x47p9aTw8E4c4A== + "@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.7": version "2.5.8" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" @@ -7635,6 +7652,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" integrity sha512-+beqKQOh9PYxuHvijhVl+tIHvT6tuwOrE9m14zd+MT2A38KoKZhh7pYJ0SNleLtwDsiIxHDsIk9bv01oOxvSvA== +"@types/semver@^7.3.8": + version "7.3.8" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59" + integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now== + "@types/serve-static@*": version "1.13.9" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" @@ -7806,6 +7828,11 @@ dependencies: "@types/node" "*" +"@types/tz-offset@*": + version "0.0.0" + resolved "https://registry.npmjs.org/@types/tz-offset/-/tz-offset-0.0.0.tgz#d58f1cebd794148d245420f8f0660305d320e565" + integrity sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ== + "@types/uglify-js@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" @@ -10847,7 +10874,7 @@ clone-stats@^1.0.0: resolved "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= -clone@2.x, clone@^2.1.1: +clone@2.x, clone@^2.1.1, clone@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= @@ -13731,6 +13758,11 @@ eventemitter2@^6.4.3: resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== +eventemitter2@^6.4.4: + version "6.4.5" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" + integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw== + eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -15641,6 +15673,11 @@ hash-base@^3.0.0: inherits "^2.0.1" safe-buffer "^5.0.1" +hash-it@^5.0.0: + version "5.0.2" + resolved "https://registry.npmjs.org/hash-it/-/hash-it-5.0.2.tgz#8cc981944964a5124f74f1065af0c0a5d182d556" + integrity sha512-csU3E/a9QEmEgPPxoShVuMcFWM329IGioEPRvYVBv3r5BFrU8pCfnk3jGEVvriAcwqd+nl6KsNhPPjg8MUzkhQ== + hash-stream-validation@^0.2.2: version "0.2.4" resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" @@ -18010,6 +18047,17 @@ json-pointer@^0.6.0: dependencies: foreach "^2.0.4" +json-rules-engine@^6.1.2: + version "6.1.2" + resolved "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-6.1.2.tgz#574ef455c10973fd5de07ea8414cbb72bb84d10f" + integrity sha512-+rtKuJ33HAvFywL9broh42FA9hkZNmS0l1DmgjP7nfGJ9E2i2IsfNH0BcXjyXianp/bXAyYlsSv308AfTuvBwQ== + dependencies: + clone "^2.1.2" + eventemitter2 "^6.4.4" + hash-it "^5.0.0" + jsonpath-plus "^5.0.7" + lodash.isobjectlike "^4.0.0" + json-schema-compare@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56" @@ -18150,6 +18198,11 @@ jsonpath-plus@^0.19.0: resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c" integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg== +jsonpath-plus@^5.0.7: + version "5.1.0" + resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.1.0.tgz#2fc4b2e461950626c98525425a3a3518b85af6c3" + integrity sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ== + jsonpointer@^4.0.1: version "4.1.0" resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" @@ -18934,6 +18987,11 @@ lodash.isobject@^3.0.2: resolved "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= +lodash.isobjectlike@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lodash.isobjectlike/-/lodash.isobjectlike-4.0.0.tgz#742c5fc65add27924d3d24191681aa9a17b2b60d" + integrity sha1-dCxfxlrdJ5JNPSQZFoGqmheytg0= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -20387,7 +20445,14 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.27.0, moment@^2.29.1: +moment-timezone@^0.5.31: + version "0.5.33" + resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" + integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.19.3, moment@^2.27.0, moment@^2.29.1: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -20686,6 +20751,13 @@ node-cache@^5.1.2: dependencies: clone "2.x" +node-cron@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" + integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== + dependencies: + moment-timezone "^0.5.31" + node-dir@^0.1.10, node-dir@^0.1.17: version "0.1.17" resolved "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" From b2363f3e3b4a54968bc76c1b5429a8fb32ce4d5d Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 21 Oct 2021 09:25:26 +0200 Subject: [PATCH 211/245] Add explicit type information to checks Types can be used to: * Determine which check logic to run * Determine the correct persistence option to use * Choose correct ser/deser logic for the check * Determine correct components to render on the frontend Modify router API to be post for better usability. The API might end up doing writes if caching etc. is added in later as well. Modify CheckRegistry & FactChecker to return persisted check when it is registered. Signed-off-by: Jussi Hallila --- .../README.md | 2 ++ .../api-report.md | 5 ++++- .../src/constants.ts | 20 +++++++++++++++++++ .../src/index.ts | 1 + .../src/service/CheckRegistry.ts | 3 ++- .../JsonRulesEngineFactChecker.test.ts | 9 ++++++++- .../src/service/JsonRulesEngineFactChecker.ts | 19 ++++++++++++++---- .../src/service/router.ts | 17 ++++++++++++---- plugins/tech-insights-common/api-report.md | 8 ++++---- plugins/tech-insights-common/src/checks.ts | 18 +++++++++++++++-- plugins/tech-insights-common/src/responses.ts | 6 ++++++ 11 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 plugins/tech-insights-backend-module-jsonfc/src/constants.ts diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index ea45a1b4b0..99570f1c7a 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -52,10 +52,12 @@ Checks for this FactChecker are constructed as `json-rules-engine` compatible JS ```ts import { TechInsightJsonRuleCheck } from '../types'; +import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; export const exampleCheck: TechInsightJsonRuleCheck = { id: 'demodatacheck', // Unique identifier of this check name: 'demodatacheck', // A human readable name of this check to be displayed in the UI + type: JSON_RULE_ENGINE_CHECK_TYPE, // Type identifier of the check. Used to run logic against, determine persistence option to use and render correct components on the UI description: 'A fact check for demoing purposes', // A description to be displayed in the UI factRefs: ['demo-poc.factretriever'], // References to fact containers that this check uses. See documentation on FactRetrievers for more information on these rule: { diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index bf08028a00..e7c612215c 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -33,6 +33,9 @@ export interface DynamicFact { options?: FactOptions; } +// @public (undocumented) +export const JSON_RULE_ENGINE_CHECK_TYPE = 'json-rules-engine'; + // @public (undocumented) export interface JsonRuleBooleanCheckResult extends BooleanCheckResult { // (undocumented) @@ -60,7 +63,7 @@ export class JsonRulesEngineFactChecker checkRegistry, }: JsonRulesEngineFactCheckerOptions); // (undocumented) - addCheck(check: TechInsightJsonRuleCheck): Promise; + addCheck(check: TechInsightJsonRuleCheck): Promise; // (undocumented) getChecks(): Promise; // (undocumented) diff --git a/plugins/tech-insights-backend-module-jsonfc/src/constants.ts b/plugins/tech-insights-backend-module-jsonfc/src/constants.ts new file mode 100644 index 0000000000..5fd69a3fef --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/constants.ts @@ -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. + */ + +/** + * @public + */ +export const JSON_RULE_ENGINE_CHECK_TYPE = 'json-rules-engine'; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/index.ts index a2561eaa3b..1a537b8257 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/index.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/index.ts @@ -17,6 +17,7 @@ export { JsonRulesEngineFactCheckerFactory, JsonRulesEngineFactChecker, } from './service/JsonRulesEngineFactChecker'; +export { JSON_RULE_ENGINE_CHECK_TYPE } from './constants'; export type { JsonRulesEngineFactCheckerFactoryOptions, JsonRulesEngineFactCheckerOptions, diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts index 2ae1ac0b83..016fb05b31 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts @@ -31,13 +31,14 @@ export class DefaultCheckRegistry }); } - async register(check: CheckType) { + async register(check: CheckType): Promise { if (this.checks.has(check.id)) { throw new ConflictError( `Tech insight check with id ${check.id} has already been registered`, ); } this.checks.set(check.id, check); + return check; } async get(checkId: string): Promise { diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts index d574e13c5f..549274cefa 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -18,7 +18,10 @@ import { TechInsightCheckRegistry, TechInsightsStore, } from '@backstage/plugin-tech-insights-common'; -import { JsonRulesEngineFactCheckerFactory } from '../index'; +import { + JSON_RULE_ENGINE_CHECK_TYPE, + JsonRulesEngineFactCheckerFactory, +} from '../index'; import { getVoidLogger } from '@backstage/backend-common'; import { TechInsightJsonRuleCheck } from '../types'; @@ -27,6 +30,7 @@ const testChecks: Record = { { id: 'brokenTestCheck', name: 'brokenTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Broken Check For Testing', factRefs: ['test-factretriever'], rule: { @@ -46,6 +50,7 @@ const testChecks: Record = { { id: 'brokenTestCheck2', name: 'brokenTestCheck2', + type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Second Broken Check For Testing', factRefs: ['non-existing-factretriever'], rule: { @@ -65,6 +70,7 @@ const testChecks: Record = { { id: 'simpleTestCheck', name: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Simple Check For Testing', factRefs: ['test-factretriever'], rule: { @@ -85,6 +91,7 @@ const testChecks: Record = { { id: 'simpleTestCheck2', name: 'simpleTestCheck2', + type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Second Simple Check For Testing', factRefs: ['test-factretriever'], rule: { diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts index 964575b494..cfc31cdffc 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -29,6 +29,7 @@ import { Logger } from 'winston'; import { pick } from 'lodash'; import Ajv from 'ajv'; import * as validationSchema from './validation-schema.json'; +import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; const noopEvent = { type: 'noop', @@ -113,6 +114,12 @@ export class JsonRulesEngineFactChecker const ajv = new Ajv({ verbose: true }); const validator = ajv.compile(validationSchema); const isValidToSchema = validator(check.rule); + if (check.type !== JSON_RULE_ENGINE_CHECK_TYPE) { + this.logger.warn( + 'Only ${JSON_RULE_ENGINE_CHECK_TYPE} checks can be registered to this fact checker', + ); + return false; + } if (!isValidToSchema) { this.logger.warn( 'Failed to to validate conditions against JSON schema', @@ -146,15 +153,18 @@ export class JsonRulesEngineFactChecker return this.checkRegistry.list(); } - async addCheck(check: TechInsightJsonRuleCheck): Promise { + async addCheck( + check: TechInsightJsonRuleCheck, + ): Promise { if (!(await this.validate(check))) { this.logger.warn( `Check validation failed when adding check ${check.name} to check registry.`, ); - return false; + throw new Error( + 'Failed to add check to rules engine. Validation failed.', + ); } - this.checkRegistry.register(check); - return true; + return await this.checkRegistry.register(check); } private retrieveFactReferences( @@ -216,6 +226,7 @@ export class JsonRulesEngineFactChecker ) { const returnable = { id: techInsightCheck.id, + type: techInsightCheck.type, name: techInsightCheck.name, description: techInsightCheck.description, factRefs: techInsightCheck.factRefs, diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index 3649f522dd..cc41868cd8 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -83,11 +83,16 @@ export async function createRouter< return res.send(await factChecker.getChecks()); }); - router.get('/checks/:namespace/:kind/:name', async (req, res) => { + router.post('/checks/run/:namespace/:kind/:name', async (req, res) => { const { namespace, kind, name } = req.params; - const checks = req.query.checks as string[]; - const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; try { + if (!('checks' in req.body)) { + return res.status(422).send({ + message: 'Failed to get checks from request.', + }); + } + const { checks }: { checks: string[] } = req.body; + const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; const checkResult = await factChecker.runChecks(entityTriplet, checks); return res.send(checkResult); } catch (e) { @@ -120,7 +125,11 @@ export async function createRouter< const startDatetime = DateTime.fromISO(req.query.startDatetime as string); const endDatetime = DateTime.fromISO(req.query.endDatetime as string); if (!startDatetime.isValid || !endDatetime.isValid) { - return res.status(422).send('Failed to parse datetime from request'); + return res.status(422).send({ + message: 'Failed to parse datetime from request', + field: !startDatetime.isValid ? 'startDateTime' : 'endDateTime', + value: !startDatetime.isValid ? startDatetime : endDatetime, + }); } const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; return res.send( diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index 6d0d40239b..40f5203fb8 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -21,6 +21,7 @@ export interface CheckResponse { id: string; metadata?: Record; name: string; + type: string; } // @public @@ -34,7 +35,7 @@ export interface FactChecker< CheckType extends TechInsightCheck, CheckResultType extends CheckResult, > { - addCheck(check: CheckType): Promise; + addCheck(check: CheckType): Promise; getChecks(): Promise; runChecks(entity: string, checks: string[]): Promise; validate(check: CheckType): Promise; @@ -108,14 +109,13 @@ export type FlatTechInsightFact = TechInsightFact & { // @public export interface TechInsightCheck { - // (undocumented) description: string; factRefs: string[]; failureMetadata?: Record; id: string; - // (undocumented) name: string; successMetadata?: Record; + type: string; } // @public @@ -127,7 +127,7 @@ export interface TechInsightCheckRegistry { // (undocumented) list(): Promise; // (undocumented) - register(check: CheckType): Promise; + register(check: CheckType): Promise; } // @public diff --git a/plugins/tech-insights-common/src/checks.ts b/plugins/tech-insights-common/src/checks.ts index eaa4792fe3..02963ab624 100644 --- a/plugins/tech-insights-common/src/checks.ts +++ b/plugins/tech-insights-common/src/checks.ts @@ -66,7 +66,7 @@ export interface FactChecker< * @param check - The actual check to be added. * @returns - An indicator if fact was successfully added */ - addCheck(check: CheckType): Promise; + addCheck(check: CheckType): Promise; /** * Retrieves all available checks that can be used to run checks against. @@ -93,7 +93,7 @@ export interface FactChecker< * */ export interface TechInsightCheckRegistry { - register(check: CheckType): Promise; + register(check: CheckType): Promise; get(checkId: string): Promise; getAll(checks: string[]): Promise; list(): Promise; @@ -135,7 +135,21 @@ export interface TechInsightCheck { */ id: string; + /** + * Type identifier for the check. + * Can be used to determine storage options, logical routing to correct FactChecker implementation + * or to help frontend render correct component types based on this + */ + type: string; + + /** + * Human readable name of the check, may be displayed in the UI + */ name: string; + + /** + * Human readable description of the check, may be displayed in the UI + */ description: string; /** diff --git a/plugins/tech-insights-common/src/responses.ts b/plugins/tech-insights-common/src/responses.ts index 983a68ee2f..0cd1d0a2e8 100644 --- a/plugins/tech-insights-common/src/responses.ts +++ b/plugins/tech-insights-common/src/responses.ts @@ -26,6 +26,12 @@ export interface CheckResponse { * Identifier of the Check */ id: string; + /** + * Type identifier for the check. + * Can be used to determine storage options, logical routing to correct FactChecker implementation + * or to help frontend render correct component types based on this + */ + type: string; /** * Human readable name of the Check */ From b427f4ba0b4b05f167f2cb261d9e83c789043207 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 21 Oct 2021 11:48:00 +0200 Subject: [PATCH 212/245] Modify validation to have a more user friendly API * Adding a container type to be returned from validate function * Modifying JsonRulesEngineFactChecker to throw with error information if check addition fails on validation Signed-off-by: Jussi Hallila --- .../api-report.md | 3 +- .../JsonRulesEngineFactChecker.test.ts | 12 +++-- .../src/service/JsonRulesEngineFactChecker.ts | 50 +++++++++++++------ plugins/tech-insights-common/api-report.md | 25 +++++++++- plugins/tech-insights-common/package.json | 1 + plugins/tech-insights-common/src/checks.ts | 39 ++++++++++++++- 6 files changed, 109 insertions(+), 21 deletions(-) diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index e7c612215c..8f6bf0cd6c 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -5,6 +5,7 @@ ```ts import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; import { CheckResponse } from '@backstage/plugin-tech-insights-common'; +import { CheckValidationResponse } from '@backstage/plugin-tech-insights-common'; import { DynamicFactCallback } from 'json-rules-engine'; import { FactChecker } from '@backstage/plugin-tech-insights-common'; import { FactOptions } from 'json-rules-engine'; @@ -72,7 +73,7 @@ export class JsonRulesEngineFactChecker checks: string[], ): Promise; // (undocumented) - validate(check: TechInsightJsonRuleCheck): Promise; + validate(check: TechInsightJsonRuleCheck): Promise; } // @public diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts index 549274cefa..124030a545 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -289,12 +289,16 @@ describe('JsonRulesEngineFactChecker', () => { describe('when validating checks', () => { it('should succeed on valid rules', async () => { - const isValid = await factChecker.validate(testChecks.simple[0]); - expect(isValid).toBeTruthy(); + const validationResponse = await factChecker.validate( + testChecks.simple[0], + ); + expect(validationResponse.valid).toBeTruthy(); }); it('should fail on broken rules', async () => { - const isValid = await factChecker.validate(testChecks.broken[0]); - expect(isValid).toBeFalsy(); + const validationResponse = await factChecker.validate( + testChecks.broken[0], + ); + expect(validationResponse.valid).toBeFalsy(); }); }); }); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts index cfc31cdffc..7e8cab8b7f 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -22,6 +22,8 @@ import { TechInsightCheckRegistry, FlatTechInsightFact, TechInsightsStore, + CheckValidationResponse, + CheckValidationError, } from '@backstage/plugin-tech-insights-common'; import { Engine, EngineResult, TopLevelCondition } from 'json-rules-engine'; import { DefaultCheckRegistry } from './CheckRegistry'; @@ -110,22 +112,31 @@ export class JsonRulesEngineFactChecker } } - async validate(check: TechInsightJsonRuleCheck): Promise { + async validate( + check: TechInsightJsonRuleCheck, + ): Promise { const ajv = new Ajv({ verbose: true }); const validator = ajv.compile(validationSchema); const isValidToSchema = validator(check.rule); if (check.type !== JSON_RULE_ENGINE_CHECK_TYPE) { - this.logger.warn( - 'Only ${JSON_RULE_ENGINE_CHECK_TYPE} checks can be registered to this fact checker', - ); - return false; + const msg = `Only ${JSON_RULE_ENGINE_CHECK_TYPE} checks can be registered to this fact checker`; + this.logger.warn(msg); + return { + valid: false, + message: msg, + }; } if (!isValidToSchema) { + const msg = 'Failed to to validate conditions against JSON schema'; this.logger.warn( 'Failed to to validate conditions against JSON schema', validator.errors, ); - return false; + return { + valid: false, + message: msg, + errors: validator.errors, + }; } const existingSchemas = await this.repository.getLatestSchemas( @@ -146,7 +157,17 @@ export class JsonRulesEngineFactChecker )}`, ); }); - return failedReferences.length === 0; + const valid = failedReferences.length === 0; + return { + valid, + ...(!valid + ? { + message: `Check is referencing missing values from fact schemas: ${failedReferences + .map(it => it.ref) + .join(',')}`, + } + : {}), + }; } getChecks(): Promise { @@ -156,13 +177,14 @@ export class JsonRulesEngineFactChecker async addCheck( check: TechInsightJsonRuleCheck, ): Promise { - if (!(await this.validate(check))) { - this.logger.warn( - `Check validation failed when adding check ${check.name} to check registry.`, - ); - throw new Error( - 'Failed to add check to rules engine. Validation failed.', - ); + const checkValidationResponse = await this.validate(check); + if (!checkValidationResponse.valid) { + const msg = `Check validation failed when adding check ${check.name} to check registry.`; + this.logger.warn(msg); + throw new CheckValidationError({ + message: checkValidationResponse.message || msg, + errors: checkValidationResponse.errors, + }); } return await this.checkRegistry.register(check); } diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index 40f5203fb8..3c599870c5 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -4,6 +4,7 @@ ```ts import { Config } from '@backstage/config'; +import { CustomErrorBase } from '@backstage/errors'; import { DateTime } from 'luxon'; import { Logger as Logger_2 } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -30,6 +31,28 @@ export type CheckResult = { check: CheckResponse; }; +// @public +export class CheckValidationError extends CustomErrorBase { + constructor({ + message, + cause, + errors, + }: { + message: string; + cause?: Error; + errors?: any; + }); + // (undocumented) + errors?: any; +} + +// @public +export type CheckValidationResponse = { + valid: boolean; + message?: string; + errors?: any; +}; + // @public export interface FactChecker< CheckType extends TechInsightCheck, @@ -38,7 +61,7 @@ export interface FactChecker< addCheck(check: CheckType): Promise; getChecks(): Promise; runChecks(entity: string, checks: string[]): Promise; - validate(check: CheckType): Promise; + validate(check: CheckType): Promise; } // @public diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 37d66a9379..15ccc1a682 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.9.0", + "@backstage/errors": "^0.1.2", "@backstage/config": "^0.1.8", "@types/luxon": "^2.0.5", "luxon": "^2.0.2", diff --git a/plugins/tech-insights-common/src/checks.ts b/plugins/tech-insights-common/src/checks.ts index 02963ab624..062a5baaaa 100644 --- a/plugins/tech-insights-common/src/checks.ts +++ b/plugins/tech-insights-common/src/checks.ts @@ -15,6 +15,7 @@ */ import { TechInsightsStore } from './persistence'; import { CheckResponse, FactResponse } from './responses'; +import { CustomErrorBase } from '@backstage/errors'; /** * A factory wrapper to construct FactChecker implementations. @@ -82,7 +83,7 @@ export interface FactChecker< * @param check - The check to be validated * @returns - Validation result */ - validate(check: CheckType): Promise; + validate(check: CheckType): Promise; } /** @@ -171,3 +172,39 @@ export interface TechInsightCheck { */ failureMetadata?: Record; } + +/** + * Validation response from CheckValidator + * + * May contain additional data for display purposes + * @public + */ +export type CheckValidationResponse = { + valid: boolean; + message?: string; + errors?: any; +}; + +/** + * Error object for Validation Errors + * + * Can be used to short circuit larger execution paths instead of passing validation response around + * + * @public + */ +export class CheckValidationError extends CustomErrorBase { + errors?: any; + + constructor({ + message, + cause, + errors, + }: { + message: string; + cause?: Error; + errors?: any; + }) { + super(message, cause); + this.errors = errors; + } +} From a1afbe0498aa4a0982efe9b448d94a59d9f419aa Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 21 Oct 2021 14:37:55 +0200 Subject: [PATCH 213/245] Use a direct custom Error since backstage/errors seems unusable. Signed-off-by: Jussi Hallila --- plugins/tech-insights-common/api-report.md | 13 ++----------- plugins/tech-insights-common/package.json | 1 - plugins/tech-insights-common/src/checks.ts | 15 +++------------ 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index 3c599870c5..db987546c3 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -4,7 +4,6 @@ ```ts import { Config } from '@backstage/config'; -import { CustomErrorBase } from '@backstage/errors'; import { DateTime } from 'luxon'; import { Logger as Logger_2 } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -32,16 +31,8 @@ export type CheckResult = { }; // @public -export class CheckValidationError extends CustomErrorBase { - constructor({ - message, - cause, - errors, - }: { - message: string; - cause?: Error; - errors?: any; - }); +export class CheckValidationError extends Error { + constructor({ message, errors }: { message: string; errors?: any }); // (undocumented) errors?: any; } diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 15ccc1a682..37d66a9379 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -31,7 +31,6 @@ }, "dependencies": { "@backstage/backend-common": "^0.9.0", - "@backstage/errors": "^0.1.2", "@backstage/config": "^0.1.8", "@types/luxon": "^2.0.5", "luxon": "^2.0.2", diff --git a/plugins/tech-insights-common/src/checks.ts b/plugins/tech-insights-common/src/checks.ts index 062a5baaaa..7b2c4c6b8f 100644 --- a/plugins/tech-insights-common/src/checks.ts +++ b/plugins/tech-insights-common/src/checks.ts @@ -15,7 +15,6 @@ */ import { TechInsightsStore } from './persistence'; import { CheckResponse, FactResponse } from './responses'; -import { CustomErrorBase } from '@backstage/errors'; /** * A factory wrapper to construct FactChecker implementations. @@ -192,19 +191,11 @@ export type CheckValidationResponse = { * * @public */ -export class CheckValidationError extends CustomErrorBase { +export class CheckValidationError extends Error { errors?: any; - constructor({ - message, - cause, - errors, - }: { - message: string; - cause?: Error; - errors?: any; - }) { - super(message, cause); + constructor({ message, errors }: { message: string; errors?: any }) { + super(message); this.errors = errors; } } From df000b95969d74b8d264543110989283650e05d6 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 25 Oct 2021 17:04:33 +0200 Subject: [PATCH 214/245] Address various code review comments. Signed-off-by: Jussi Hallila --- .changeset/bright-pandas-rush.md | 17 --- packages/backend/src/index.ts | 2 +- packages/backend/src/plugins/techInsights.ts | 71 +++++++++- .../CHANGELOG.md | 7 - .../README.md | 2 +- .../api-report.md | 14 -- .../package.json | 4 +- .../src/index.ts | 1 - .../JsonRulesEngineFactChecker.test.ts | 99 +++++++------- .../src/service/JsonRulesEngineFactChecker.ts | 46 +++---- .../src/types.ts | 16 +-- plugins/tech-insights-backend/CHANGELOG.md | 7 - plugins/tech-insights-backend/README.md | 2 +- plugins/tech-insights-backend/api-report.md | 19 +-- .../migrations/202109061111_fact_schemas.js | 17 ++- .../migrations/202109061212_facts.js | 21 ++- plugins/tech-insights-backend/src/index.ts | 4 +- .../service/fact/FactRetrieverEngine.test.ts | 45 +++---- .../src/service/fact/FactRetrieverEngine.ts | 22 +-- .../src/service/fact/FactRetrieverRegistry.ts | 8 +- .../persistence/TechInsightsDatabase.test.ts | 127 ++++++++++++------ .../persistence/TechInsightsDatabase.ts | 94 ++++++------- .../src/service/router.test.ts | 51 +++---- .../src/service/router.ts | 44 ++++-- ...ilder.ts => techInsightsContextBuilder.ts} | 91 ++++++------- plugins/tech-insights-common/CHANGELOG.md | 7 - plugins/tech-insights-common/api-report.md | 55 ++++---- plugins/tech-insights-common/src/checks.ts | 2 +- plugins/tech-insights-common/src/facts.ts | 64 +++++---- .../tech-insights-common/src/persistence.ts | 32 +++-- plugins/tech-insights-common/src/responses.ts | 11 +- 31 files changed, 529 insertions(+), 473 deletions(-) delete mode 100644 .changeset/bright-pandas-rush.md delete mode 100644 plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md delete mode 100644 plugins/tech-insights-backend/CHANGELOG.md rename plugins/tech-insights-backend/src/service/{DefaultTechInsightsBuilder.ts => techInsightsContextBuilder.ts} (63%) delete mode 100644 plugins/tech-insights-common/CHANGELOG.md diff --git a/.changeset/bright-pandas-rush.md b/.changeset/bright-pandas-rush.md deleted file mode 100644 index 86b3b819ee..0000000000 --- a/.changeset/bright-pandas-rush.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/plugin-tech-insights-backend': minor -'@backstage/plugin-tech-insights-backend-module-jsonfc': minor -'@backstage/plugin-tech-insights-common': minor ---- - -## Add initial implementation of Tech Insights backend - -Add common types and interfaces for Tech Insights. Exposing components needed to use and modify tech-insights-backend module and to implement individual Fact Retrievers, Fact Checkers and their persistence options. - -Implement a framework to run fact retrievers and store fact data into the database. Add migration scripts to create a new database for `tech_insights`. - -Create a default implementation of a FactChecker enabling users to construct checks and run them and generate scorecards based on checks. - -To be able to use tech insights in your application you need to implement `FactRetriever`s to retrieve and return data for facts and register them to the tech-insights-backend. If you want to use fact checking functionality, you need to create `check`s and register them to an implementation of a `FactChecker`. - -For more information see documentation on the README.md files of the respective packages. diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 6d40072f99..ffdce949b7 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -109,7 +109,7 @@ async function main() { const badgesEnv = useHotMemoize(module, () => createEnv('badges')); const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); const techInsightsEnv = useHotMemoize(module, () => - createEnv('tech_insights'), + createEnv('tech-insights'), ); const apiRouter = Router(); diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index c396f4b82b..d51c0c7575 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -15,11 +15,15 @@ */ import { createRouter, - DefaultTechInsightsBuilder, + buildTechInsightsContext, } from '@backstage/plugin-tech-insights-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; +import { + JSON_RULE_ENGINE_CHECK_TYPE, + JsonRulesEngineFactCheckerFactory, +} from '@backstage/plugin-tech-insights-backend-module-jsonfc'; +import { CatalogClient } from '@backstage/catalog-client'; export default async function createPlugin({ logger, @@ -27,20 +31,75 @@ export default async function createPlugin({ discovery, database, }: PluginEnvironment): Promise { - const builder = new DefaultTechInsightsBuilder({ + const techInsightsContext = await buildTechInsightsContext({ logger, config, database, discovery, - factRetrievers: [], + factRetrievers: [ + { + cadence: '1 1 1 * *', // At 01:01 on day-of-month 1. + factRetriever: { + id: 'testRetriever', + version: '1.1.1', + entityTypes: ['component'], + schema: { + examplenumberfact: { + type: 'integer', + description: '', + }, + }, + handler: async _ctx => { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities(); + + return Promise.resolve( + entities.items.map(it => { + return { + entity: { + namespace: it.metadata.namespace!!, + kind: it.kind, + name: it.metadata.name, + }, + facts: { + examplenumberfact: 2, + }, + }; + }), + ); + }, + }, + }, + ], factCheckerFactory: new JsonRulesEngineFactCheckerFactory({ - checks: [], + checks: [ + { + id: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factIds: ['testRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'examplenumberfact', + operator: 'lessThan', + value: 5, + }, + ], + }, + }, + }, + ], logger, }), }); return await createRouter({ - ...(await builder.build()), + ...techInsightsContext, logger, config, }); diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md deleted file mode 100644 index d0aced5bf5..0000000000 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @backstage/plugin-tech-insights-backend-module-jsonfc - -## 0.0.1 - -### Patch Changes - -- Initial implementation diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index 99570f1c7a..1404bd4ea4 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -2,7 +2,7 @@ This is an extension to module to tech-insights-backend plugin, which provides basic framework and functionality to implement tech insights within Backstage. -This module provides functionality to run checks against a `json-rules-engine` and provide boolean logic by simply building checks using JSON conditions. +This module provides functionality to run checks against a [json-rules-engine](https://github.com/CacheControl/json-rules-engine) and provide boolean logic by simply building checks using JSON conditions. ## Getting started diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index 8f6bf0cd6c..3d214566c5 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -6,9 +6,7 @@ import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; import { CheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckValidationResponse } from '@backstage/plugin-tech-insights-common'; -import { DynamicFactCallback } from 'json-rules-engine'; import { FactChecker } from '@backstage/plugin-tech-insights-common'; -import { FactOptions } from 'json-rules-engine'; import { Logger as Logger_2 } from 'winston'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-common'; import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-common'; @@ -24,16 +22,6 @@ export type CheckCondition = { result: boolean; }; -// @public (undocumented) -export interface DynamicFact { - // (undocumented) - calculationMethod: DynamicFactCallback | T; - // (undocumented) - id: string; - // (undocumented) - options?: FactOptions; -} - // @public (undocumented) export const JSON_RULE_ENGINE_CHECK_TYPE = 'json-rules-engine'; @@ -120,8 +108,6 @@ export type Rule = { // @public (undocumented) export interface TechInsightJsonRuleCheck extends TechInsightCheck { - // (undocumented) - dynamicFacts?: DynamicFact[]; // (undocumented) rule: Rule; } diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 945947dd3b..c11f20ee38 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,8 +1,8 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", "version": "0.1.0", - "main": "src/index.ts", - "types": "src/index.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts", "license": "Apache-2.0", "private": false, "publishConfig": { diff --git a/plugins/tech-insights-backend-module-jsonfc/src/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/index.ts index 1a537b8257..1239211f73 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/index.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/index.ts @@ -28,6 +28,5 @@ export type { TechInsightJsonRuleCheck, ResponseTopLevelCondition, Rule, - DynamicFact, CheckCondition, } from './types'; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts index 124030a545..a31ae90c70 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -32,7 +32,7 @@ const testChecks: Record = { name: 'brokenTestCheck', type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Broken Check For Testing', - factRefs: ['test-factretriever'], + factIds: ['test-factretriever'], rule: { conditions: { all: [ @@ -52,7 +52,7 @@ const testChecks: Record = { name: 'brokenTestCheck2', type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Second Broken Check For Testing', - factRefs: ['non-existing-factretriever'], + factIds: ['non-existing-factretriever'], rule: { conditions: { any: [ @@ -72,7 +72,7 @@ const testChecks: Record = { name: 'simpleTestCheck', type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Simple Check For Testing', - factRefs: ['test-factretriever'], + factIds: ['test-factretriever'], rule: { conditions: { all: [ @@ -93,7 +93,7 @@ const testChecks: Record = { name: 'simpleTestCheck2', type: JSON_RULE_ENGINE_CHECK_TYPE, description: 'Second Simple Check For Testing', - factRefs: ['test-factretriever'], + factIds: ['test-factretriever'], rule: { conditions: { all: [ @@ -114,17 +114,15 @@ const latestSchemasMock = jest.fn().mockImplementation(() => [ version: '0.0.1', id: 2, ref: 'test-factretriever', - schema: { - testnumberfact: { - type: 'integer', - description: '', - entityKinds: ['component'], - }, + entityTypes: ['component'], + testnumberfact: { + type: 'integer', + description: '', }, }, ]); -const factsBetweenTimestampsForRefsMock = jest.fn(); -const latestFactsForRefsMock = jest.fn().mockImplementation(() => ({})); +const factsBetweenTimestampsByIdsMock = jest.fn(); +const latestFactsByIdsMock = jest.fn().mockImplementation(() => ({})); const mockCheckRegistry = { getAll(checks: string[]) { return checks.flatMap(check => testChecks[check]); @@ -132,8 +130,8 @@ const mockCheckRegistry = { } as unknown as TechInsightCheckRegistry; const mockRepository: TechInsightsStore = { - getLatestFactsForRefs: latestFactsForRefsMock, - getFactsBetweenTimestampsForRefs: factsBetweenTimestampsForRefsMock, + getLatestFactsByIds: latestFactsByIdsMock, + getFactsBetweenTimestampsByIds: factsBetweenTimestampsByIdsMock, getLatestSchemas: latestSchemasMock, } as unknown as TechInsightsStore; @@ -159,10 +157,10 @@ describe('JsonRulesEngineFactChecker', () => { ); }); it('should respond with result, facts, fact schemas and checks', async () => { - latestFactsForRefsMock.mockImplementation(() => + latestFactsByIdsMock.mockImplementation(() => Promise.resolve({ ['test-factretriever']: { - ref: 'test-factretriever', + id: 'test-factretriever', facts: { testnumberfact: 3, }, @@ -170,46 +168,45 @@ describe('JsonRulesEngineFactChecker', () => { }), ); const results = await factChecker.runChecks('a/a/a', ['simple']); - expect(results).toMatchObject([ - { - facts: { - testnumberfact: { - value: 3, - type: 'integer', - description: '', - entityKinds: ['component'], - }, + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', }, - result: true, - check: { - id: 'simpleTestCheck', - name: 'simpleTestCheck', - description: 'Simple Check For Testing', - factRefs: ['test-factretriever'], - rule: { - conditions: { - all: [ - { - fact: 'testnumberfact', - factResult: 3, - operator: 'lessThan', - result: true, - value: 5, - }, - ], - priority: 1, - }, + }, + result: true, + check: { + id: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + factResult: 3, + operator: 'lessThan', + result: true, + value: 5, + }, + ], + priority: 1, }, }, }, - ]); + }); }); it('should gracefully handle multiple check at once', async () => { - latestFactsForRefsMock.mockImplementation(() => + latestFactsByIdsMock.mockImplementation(() => Promise.resolve({ ['test-factretriever']: { - ref: 'test-factretriever', + id: 'test-factretriever', facts: { testnumberfact: 3, }, @@ -227,15 +224,15 @@ describe('JsonRulesEngineFactChecker', () => { value: 3, type: 'integer', description: '', - entityKinds: ['component'], }, }, result: true, check: { id: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, name: 'simpleTestCheck', description: 'Simple Check For Testing', - factRefs: ['test-factretriever'], + factIds: ['test-factretriever'], rule: { conditions: { priority: 1, @@ -258,15 +255,15 @@ describe('JsonRulesEngineFactChecker', () => { value: 3, type: 'integer', description: '', - entityKinds: ['component'], }, }, result: true, check: { id: 'simpleTestCheck2', + type: JSON_RULE_ENGINE_CHECK_TYPE, name: 'simpleTestCheck2', description: 'Second Simple Check For Testing', - factRefs: ['test-factretriever'], + factIds: ['test-factretriever'], rule: { conditions: { priority: 1, diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts index 7e8cab8b7f..ab90a3d828 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -18,7 +18,6 @@ import { JsonRuleBooleanCheckResult, TechInsightJsonRuleCheck } from '../types'; import { FactChecker, FactResponse, - FactValueDefinitions, TechInsightCheckRegistry, FlatTechInsightFact, TechInsightsStore, @@ -81,20 +80,13 @@ export class JsonRulesEngineFactChecker ): Promise { const engine = new Engine(); const techInsightChecks = await this.checkRegistry.getAll(checks); - const factRefs = techInsightChecks.flatMap(it => it.factRefs); - const facts = await this.repository.getLatestFactsForRefs(factRefs, entity); + const factIds = techInsightChecks.flatMap(it => it.factIds); + const facts = await this.repository.getLatestFactsByIds(factIds, entity); techInsightChecks.forEach(techInsightCheck => { const rule = techInsightCheck.rule; rule.name = techInsightCheck.id; engine.addRule({ ...techInsightCheck.rule, event: noopEvent }); - - if (techInsightCheck.dynamicFacts) { - techInsightCheck.dynamicFacts.forEach(it => - engine.addFact(it.id, it.calculationMethod, it.options), - ); - } }); - const factValues = Object.values(facts).reduce( (acc, it) => ({ ...acc, ...it.facts }), {}, @@ -140,21 +132,21 @@ export class JsonRulesEngineFactChecker } const existingSchemas = await this.repository.getLatestSchemas( - check.factRefs, + check.factIds, + ); + const references = this.retrieveIndividualFactReferences( + check.rule.conditions, ); - const references = this.retrieveFactReferences(check.rule.conditions); const results = references.map(ref => ({ ref, - result: existingSchemas.some(schema => schema.schema.hasOwnProperty(ref)), + result: existingSchemas.some(schema => schema.hasOwnProperty(ref)), })); const failedReferences = results.filter(it => !it.result); failedReferences.forEach(it => { this.logger.warn( `Validation failed for check ${check.name}. Reference to value ${ it.ref - } does not exists in referred fact schemas: ${check.factRefs.join( - ',', - )}`, + } does not exists in referred fact schemas: ${check.factIds.join(',')}`, ); }); const valid = failedReferences.length === 0; @@ -189,17 +181,21 @@ export class JsonRulesEngineFactChecker return await this.checkRegistry.register(check); } - private retrieveFactReferences( + private retrieveIndividualFactReferences( condition: TopLevelCondition | { fact: string }, ): string[] { let results: string[] = []; if ('all' in condition) { results = results.concat( - condition.all.flatMap(con => this.retrieveFactReferences(con)), + condition.all.flatMap(con => + this.retrieveIndividualFactReferences(con), + ), ); } else if ('any' in condition) { results = results.concat( - condition.any.flatMap(con => this.retrieveFactReferences(con)), + condition.any.flatMap(con => + this.retrieveIndividualFactReferences(con), + ), ); } else { results.push(condition.fact); @@ -251,7 +247,7 @@ export class JsonRulesEngineFactChecker type: techInsightCheck.type, name: techInsightCheck.name, description: techInsightCheck.description, - factRefs: techInsightCheck.factRefs, + factIds: techInsightCheck.factIds, metadata: result.result ? techInsightCheck.successMetadata : techInsightCheck.failureMetadata, @@ -272,18 +268,18 @@ export class JsonRulesEngineFactChecker techInsightCheck: TechInsightJsonRuleCheck, ): Promise { const factSchemas = await this.repository.getLatestSchemas( - techInsightCheck.factRefs, + techInsightCheck.factIds, ); - const schemas: FactValueDefinitions = factSchemas.reduce( - (acc, schema) => ({ ...acc, ...schema.schema }), + const schemas = factSchemas.reduce( + (acc, schema) => ({ ...acc, ...schema }), {}, ); - const individualFacts = this.retrieveFactReferences( + const individualFacts = this.retrieveIndividualFactReferences( techInsightCheck.rule.conditions, ); const factValues = facts .filter(factContainer => - techInsightCheck.factRefs.includes(factContainer.ref), + techInsightCheck.factIds.includes(factContainer.id), ) .reduce( (acc, factContainer) => ({ diff --git a/plugins/tech-insights-backend-module-jsonfc/src/types.ts b/plugins/tech-insights-backend-module-jsonfc/src/types.ts index 5f6210d110..23d5849eda 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/types.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/types.ts @@ -13,26 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DynamicFactCallback, - FactOptions, - TopLevelCondition, -} from 'json-rules-engine'; +import { TopLevelCondition } from 'json-rules-engine'; import { BooleanCheckResult, CheckResponse, TechInsightCheck, } from '@backstage/plugin-tech-insights-common'; -/** - * @public - */ -export interface DynamicFact { - id: string; - calculationMethod: DynamicFactCallback | T; - options?: FactOptions; -} - /** * @public */ @@ -47,7 +34,6 @@ export type Rule = { */ export interface TechInsightJsonRuleCheck extends TechInsightCheck { rule: Rule; - dynamicFacts?: DynamicFact[]; } /** diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md deleted file mode 100644 index 17e0211ab0..0000000000 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @backstage/plugin-tech-insights-backend - -## 0.0.1 - -### Patch Changes - -- Initial implementation diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 4e5b5ad1b3..32265c1ced 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -124,7 +124,7 @@ const myFactRetriever: FactRetriever = { examplenumberfact: { type: 'integer', // Type of the fact description: 'A fact of a number', // Description of the fact - entityKinds: ['component'], // An array of entity kinds that this fact is applicable to + entityTypes: ['component'], // An array of entity kinds that this fact is applicable to }, }, }, diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index fc1bd9217b..6bd161c092 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -15,21 +15,22 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-common'; import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +// Warning: (ae-missing-release-tag) "buildTechInsightsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const buildTechInsightsContext: < + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>( + options: TechInsightsOptions, +) => Promise>; + // @public export function createRouter< CheckType extends TechInsightCheck, CheckResultType extends CheckResult, >(options: RouterOptions): Promise; -// @public (undocumented) -export class DefaultTechInsightsBuilder< - CheckType extends TechInsightCheck, - CheckResultType extends CheckResult, -> { - constructor(options: TechInsightsOptions); - build(): Promise>; -} - // @public export type PersistenceContext = { techInsightsStore: TechInsightsStore; diff --git a/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js index 4afe47061f..64f6c11e83 100644 --- a/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js +++ b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js @@ -24,24 +24,28 @@ exports.up = async function up(knex) { table.comment( 'The table for tech insight fact schemas. Containing a versioned data model definition for a collection of facts.', ); - table.increments('id').primary(); table - .text('ref') + .text('id') .notNullable() .comment('Identifier of the fact retriever plugin/package'); table .string('version') .notNullable() .comment('SemVer string defining the version of schema.'); + table + .string('entityTypes') + .nullable() + .comment( + 'A comma separated collection of entity kinds the fact retriever providing this schema affects. Defaults to null, which means all entity kinds.', + ); table .text('schema') .notNullable() .comment( 'Fact schema defining the values/types what this version of the fact would contain.', ); - - table.index('ref', 'fact_schema_ref_idx'); - table.index(['ref', 'version'], 'fact_schema_ref_version_idx'); + table.primary(['id', 'version']); + table.index('id', 'fact_schema_id_idx'); }); }; @@ -50,8 +54,7 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { await knex.schema.alterTable('fact_schemas', table => { - table.dropIndex([], 'fact_schema_ref_idx'); - table.dropIndex([], 'fact_schema_ref_version_idx'); + table.dropIndex([], 'fact_schema_id_idx'); }); await knex.schema.dropTable('fact_schemas'); }; diff --git a/plugins/tech-insights-backend/migrations/202109061212_facts.js b/plugins/tech-insights-backend/migrations/202109061212_facts.js index f80883ef1d..0bdcdc0da1 100644 --- a/plugins/tech-insights-backend/migrations/202109061212_facts.js +++ b/plugins/tech-insights-backend/migrations/202109061212_facts.js @@ -25,11 +25,7 @@ exports.up = async function up(knex) { 'The table for tech insight fact collections. Contains facts for individual fact retriever namespace/ref.', ); table - .bigIncrements('index') - .notNullable() - .comment('An insert counter to ensure ordering'); - table - .text('ref') + .text('id') .notNullable() .comment('Unique identifier of the fact retriever plugin/package'); table @@ -54,9 +50,13 @@ exports.up = async function up(knex) { 'Values of the fact collection stored as key-value pairs in JSON format.', ); - table.index('index', 'fact_index_idx'); - table.index('ref', 'fact_ref_idx'); - table.index(['ref', 'entity'], 'fact_ref_entity_idx'); + table + .foreign(['id', 'version']) + .references(['id', 'version']) + .inTable('fact_schemas'); + + table.index(['id', 'entity'], 'fact_id_entity_idx'); + table.index('id', 'fact_id_idx'); }); }; @@ -65,9 +65,8 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { await knex.schema.alterTable('facts', table => { - table.dropIndex([], 'facts_index_idx'); - table.dropIndex([], 'fact_ref_idx'); - table.dropIndex([], 'fact_ref_entity_idx'); + table.dropIndex([], 'fact_id_idx'); + table.dropIndex([], 'fact_id_entity_idx'); }); await knex.schema.dropTable('facts'); }; diff --git a/plugins/tech-insights-backend/src/index.ts b/plugins/tech-insights-backend/src/index.ts index eb54e8cfed..fd7f3ccd53 100644 --- a/plugins/tech-insights-backend/src/index.ts +++ b/plugins/tech-insights-backend/src/index.ts @@ -17,10 +17,10 @@ export * from './service/router'; export type { RouterOptions } from './service/router'; -export { DefaultTechInsightsBuilder } from './service/DefaultTechInsightsBuilder'; +export { buildTechInsightsContext } from './service/techInsightsContextBuilder'; export type { TechInsightsOptions, TechInsightsContext, -} from './service/DefaultTechInsightsBuilder'; +} from './service/techInsightsContextBuilder'; export type { PersistenceContext } from './service/persistence/DatabaseManager'; diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 6b9b58e3b8..ef53ae8412 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -16,7 +16,7 @@ import { FactRetriever, FactRetrieverRegistration, - FactSchema, + FactSchemaDefinition, TechInsightFact, TechInsightsStore, } from '@backstage/plugin-tech-insights-common'; @@ -35,21 +35,18 @@ jest.mock('node-cron', () => { }); const testFactRetriever: FactRetriever = { - ref: 'test-factretriever', + id: 'test-factretriever', + version: '0.0.1', + entityTypes: ['component'], schema: { - version: '0.0.1', - schema: { - testnumberfact: { - type: 'integer', - description: '', - entityKinds: ['component'], - }, + testnumberfact: { + type: 'integer', + description: '', }, }, handler: async () => { return [ { - ref: 'test-factretriever', entity: { namespace: 'a', kind: 'a', @@ -65,7 +62,9 @@ const testFactRetriever: FactRetriever = { const cadence = '1 * * * *'; describe('FactRetrieverEngine', () => { let engine: FactRetrieverEngine; - let factSchemaAssertionCallback: (ref: string, schema: FactSchema) => void; + let factSchemaAssertionCallback: ( + factSchemaDefinition: FactSchemaDefinition, + ) => void; let factInsertionAssertionCallback: (facts: TechInsightFact[]) => void; const mockRepository: TechInsightsStore = { @@ -73,8 +72,8 @@ describe('FactRetrieverEngine', () => { factInsertionAssertionCallback(facts); return Promise.resolve(); }, - insertFactSchema: (ref: string, schema: FactSchema) => { - factSchemaAssertionCallback(ref, schema); + insertFactSchema: (def: FactSchemaDefinition) => { + factSchemaAssertionCallback(def); return Promise.resolve(); }, } as unknown as TechInsightsStore; @@ -102,21 +101,19 @@ describe('FactRetrieverEngine', () => { }; it('Should update fact retriever schemas on initialization', async () => { - factSchemaAssertionCallback = (ref, schema) => { - expect(ref).toEqual('test-factretriever'); + factSchemaAssertionCallback = ({ id, schema, version, entityTypes }) => { + expect(id).toEqual('test-factretriever'); + expect(version).toEqual('0.0.1'); + expect(entityTypes).toEqual(['component']); expect(schema).toEqual({ - version: '0.0.1', - schema: { - testnumberfact: { - type: 'integer', - description: '', - entityKinds: ['component'], - }, + testnumberfact: { + type: 'integer', + description: '', }, }); }; - engine = await FactRetrieverEngine.fromConfig(defaultEngineConfig); + engine = await FactRetrieverEngine.create(defaultEngineConfig); }); it('Should insert facts when scheduled step is run', async () => { (schedule as jest.Mock).mockImplementation( @@ -143,7 +140,7 @@ describe('FactRetrieverEngine', () => { }, }); }; - engine = await FactRetrieverEngine.fromConfig(defaultEngineConfig); + engine = await FactRetrieverEngine.create(defaultEngineConfig); engine.schedule(); const job: any = engine.getJob('test-factretriever'); job.triggerScheduledJobNow(); diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts index 01e3c12370..6dc3396819 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -45,7 +45,7 @@ export class FactRetrieverEngine { private readonly defaultCadence?: string, ) {} - static async fromConfig({ + static async create({ repository, factRetrieverRegistry, factRetrieverContext, @@ -59,7 +59,7 @@ export class FactRetrieverEngine { await Promise.all( factRetrieverRegistry .listRetrievers() - .map(it => repository.insertFactSchema(it.ref, it.schema)), + .map(it => repository.insertFactSchema(it)), ); return new FactRetrieverEngine( @@ -76,12 +76,12 @@ export class FactRetrieverEngine { const newRegs: string[] = []; registrations.forEach(registration => { const { factRetriever, cadence } = registration; - if (!this.scheduledJobs.has(factRetriever.ref)) { + if (!this.scheduledJobs.has(factRetriever.id)) { const cronExpression = cadence || this.defaultCadence || randomDailyCron(); if (!validate(cronExpression)) { this.logger.warn( - `Validation failed for cron expression ${cronExpression} when trying to schedule fact retriever ${factRetriever.ref}`, + `Validation failed for cron expression ${cronExpression} when trying to schedule fact retriever ${factRetriever.id}`, ); return; } @@ -89,8 +89,8 @@ export class FactRetrieverEngine { cronExpression, this.createFactRetrieverHandler(factRetriever), ); - this.scheduledJobs.set(factRetriever.ref, job); - newRegs.push(factRetriever.ref); + this.scheduledJobs.set(factRetriever.id, job); + newRegs.push(factRetriever.id); } }); this.logger.info( @@ -106,27 +106,27 @@ export class FactRetrieverEngine { return async () => { const startTimestamp = process.hrtime(); this.logger.info( - `Retrieving facts for fact retriever ${factRetriever.ref}`, + `Retrieving facts for fact retriever ${factRetriever.id}`, ); const facts = await factRetriever.handler(this.factRetrieverContext); if (this.logger.isDebugEnabled()) { this.logger.debug( `Retrieved ${facts.length} facts for fact retriever ${ - factRetriever.ref + factRetriever.id } in ${duration(startTimestamp)}`, ); } try { - await this.repository.insertFacts(factRetriever.ref, facts); + await this.repository.insertFacts(factRetriever.id, facts); this.logger.info( `Stored ${facts.length} facts for fact retriever ${ - factRetriever.ref + factRetriever.id } in ${duration(startTimestamp)}`, ); } catch (e) { this.logger.warn( - `Failed to insert facts for fact retriever ${factRetriever.ref}`, + `Failed to insert facts for fact retriever ${factRetriever.id}`, e, ); } diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts index c76bb6039d..59d1483720 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -31,19 +31,19 @@ export class FactRetrieverRegistry { } register(registration: FactRetrieverRegistration) { - if (this.retrievers.has(registration.factRetriever.ref)) { + if (this.retrievers.has(registration.factRetriever.id)) { throw new ConflictError( - `Tech insight fact retriever with reference '${registration.factRetriever.ref}' has already been registered`, + `Tech insight fact retriever with identifier '${registration.factRetriever.id}' has already been registered`, ); } - this.retrievers.set(registration.factRetriever.ref, registration); + this.retrievers.set(registration.factRetriever.id, registration); } get(retrieverReference: string): FactRetriever { const registration = this.retrievers.get(retrieverReference); if (!registration) { throw new NotFoundError( - `Tech insight fact retriever with reference '${retrieverReference}' is not registered.`, + `Tech insight fact retriever with identifier '${retrieverReference}' is not registered.`, ); } return registration.factRetriever; diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts index ab54cf819d..44a376d5da 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts @@ -20,47 +20,58 @@ import { Knex } from 'knex'; const factSchemas = [ { - ref: 'test-schema', + id: 'test-fact', version: '0.0.1-test', + entityTypes: ['component'], schema: JSON.stringify({ testNumberFact: { type: 'integer', description: 'Test fact with a number type', - entityKinds: ['component'], }, }), }, ]; const additionalFactSchemas = [ { - ref: 'test-schema', + id: 'test-fact', version: '1.2.1-test', + entityTypes: ['component'], schema: JSON.stringify({ testNumberFact: { type: 'integer', description: 'Test fact with a number type', - entityKinds: ['component'], }, testStringFact: { type: 'string', description: 'Test fact with a string type', - entityKinds: ['service'], }, }), }, { - ref: 'test-schema', + id: 'test-fact', version: '1.1.1-test', + entityTypes: ['component'], schema: JSON.stringify({ testStringFact: { type: 'string', description: 'Test fact with a string type', - entityKinds: ['service'], }, }), }, ]; +const secondSchema = { + id: 'second-test-fact', + version: '0.0.1-test', + entityTypes: ['service'], + schema: JSON.stringify({ + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }), +}; + const now = DateTime.now().toISO(); const shortlyInTheFuture = DateTime.now() .plus(Duration.fromMillis(555)) @@ -72,18 +83,18 @@ const farInTheFuture = DateTime.now() const facts = [ { timestamp: now, - ref: 'test-fact', + id: 'test-fact', version: '0.0.1-test', - entity: 'a/a/a', + entity: 'a:a/a', facts: JSON.stringify({ testNumberFact: 1, }), }, { timestamp: shortlyInTheFuture, - ref: 'test-fact', + id: 'test-fact', version: '0.0.1-test', - entity: 'a/a/a', + entity: 'a:a/a', facts: JSON.stringify({ testNumberFact: 2, }), @@ -93,9 +104,9 @@ const facts = [ const additionalFacts = [ { timestamp: farInTheFuture, - ref: 'test-fact', + id: 'test-fact', version: '0.0.1-test', - entity: 'a/a/a', + entity: 'a:a/a', facts: JSON.stringify({ testNumberFact: 3, }), @@ -114,7 +125,7 @@ describe('Tech Insights database', () => { }); const baseAssertionFact = { - ref: 'test-fact', + id: 'test-fact', entity: { namespace: 'a', kind: 'a', name: 'a' }, timestamp: DateTime.fromISO(shortlyInTheFuture), version: '0.0.1-test', @@ -124,14 +135,12 @@ describe('Tech Insights database', () => { it('should be able to return latest schema', async () => { const schemas = await store.getLatestSchemas(); expect(schemas[0]).toMatchObject({ - ref: 'test-schema', + id: 'test-fact', version: '0.0.1-test', - schema: { - testNumberFact: { - type: 'integer', - description: 'Test fact with a number type', - entityKinds: ['component'], - }, + entityTypes: ['component'], + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', }, }); }); @@ -141,43 +150,75 @@ describe('Tech Insights database', () => { const schemas = await store.getLatestSchemas(); expect(schemas[0]).toMatchObject({ - ref: 'test-schema', + id: 'test-fact', version: '1.2.1-test', - schema: { - testNumberFact: { - type: 'integer', - description: 'Test fact with a number type', - entityKinds: ['component'], - }, - testStringFact: { - type: 'string', - description: 'Test fact with a string type', - entityKinds: ['service'], - }, + entityTypes: ['component'], + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', }, }); }); - it('should return latest facts only for the correct ref', async () => { - const returnedFact = await store.getLatestFactsForRefs( + it('should return multiple schemas if those exists', async () => { + await testDbClient.batchInsert('fact_schemas', [ + { + ...secondSchema, + id: 'second', + }, + ]); + + const schemas = await store.getLatestSchemas(); + expect(schemas).toHaveLength(2); + expect(schemas[0]).toMatchObject({ + id: 'test-fact', + version: '1.2.1-test', + entityTypes: ['component'], + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }); + expect(schemas[1]).toMatchObject({ + id: 'second', + version: '0.0.1-test', + entityTypes: ['service'], + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }); + }); + + it('should return latest facts only for the correct id', async () => { + const returnedFact = await store.getLatestFactsByIds( ['test-fact'], - 'a/a/a', + 'a:a/a', ); expect(returnedFact['test-fact']).toMatchObject(baseAssertionFact); }); - it('should return latest facts for multiple refs', async () => { + it('should return latest facts for multiple ids', async () => { + await testDbClient.batchInsert('fact_schemas', [secondSchema]); await testDbClient.batchInsert( 'facts', additionalFacts.map(fact => ({ ...fact, - ref: 'second-test-fact', + id: 'second-test-fact', timestamp: farInTheFuture, })), ); - const returnedFacts = await store.getLatestFactsForRefs( + const returnedFacts = await store.getLatestFactsByIds( ['test-fact', 'second-test-fact'], - 'a/a/a', + 'a:a/a', ); expect(returnedFacts['test-fact']).toMatchObject({ @@ -185,7 +226,7 @@ describe('Tech Insights database', () => { }); expect(returnedFacts['second-test-fact']).toMatchObject({ ...baseAssertionFact, - ref: 'second-test-fact', + id: 'second-test-fact', timestamp: DateTime.fromISO(farInTheFuture), facts: { testNumberFact: 3 }, }); @@ -193,9 +234,9 @@ describe('Tech Insights database', () => { it('should return facts correctly between time range', async () => { await testDbClient.batchInsert('facts', additionalFacts); - const returnedFacts = await store.getFactsBetweenTimestampsForRefs( + const returnedFacts = await store.getFactsBetweenTimestampsByIds( ['test-fact'], - 'a/a/a', + 'a:a/a', DateTime.fromISO(now), DateTime.fromISO(shortlyInTheFuture).plus(Duration.fromMillis(10)), ); diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts index 1bdf18249f..af45539b32 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -19,14 +19,16 @@ import { TechInsightFact, FlatTechInsightFact, TechInsightsStore, + FactSchemaDefinition, } from '@backstage/plugin-tech-insights-common'; import { rsort } from 'semver'; -import { groupBy } from 'lodash'; +import { groupBy, omit } from 'lodash'; import { DateTime } from 'luxon'; import { Logger } from 'winston'; +import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model'; export type RawDbFactRow = { - ref: string; + id: string; version: string; timestamp: Date | string; entity: string; @@ -34,10 +36,10 @@ export type RawDbFactRow = { }; type RawDbFactSchemaRow = { - id: number; - ref: string; + id: string; version: string; schema: string; + entityTypes?: string; }; export class TechInsightsDatabase implements TechInsightsStore { @@ -45,51 +47,51 @@ export class TechInsightsDatabase implements TechInsightsStore { constructor(private readonly db: Knex, private readonly logger: Logger) {} - async getLatestSchemas(refs?: string[]): Promise { + async getLatestSchemas(ids?: string[]): Promise { const queryBuilder = this.db('fact_schemas'); - if (refs) { - queryBuilder.whereIn('ref', refs); + if (ids) { + queryBuilder.whereIn('id', ids); } const existingSchemas = await queryBuilder.orderBy('id', 'desc').select(); - const groupedSchemas = groupBy(existingSchemas, 'ref'); + const groupedSchemas = groupBy(existingSchemas, 'id'); return Object.values(groupedSchemas) .map(schemas => { const sorted = rsort(schemas.map(it => it.version)); return schemas.find(it => it.version === sorted[0])!!; }) .map((it: RawDbFactSchemaRow) => ({ - ...it, - schema: JSON.parse(it.schema), + ...omit(it, 'schema'), + ...JSON.parse(it.schema), + entityTypes: it.entityTypes ? it.entityTypes.split(',') : [], })); } - async insertFactSchema(ref: string, schema: FactSchema) { + async insertFactSchema(schemaDefinition: FactSchemaDefinition) { + const { id, version, schema, entityTypes } = schemaDefinition; const existingSchemas = await this.db('fact_schemas') - .where({ ref }) + .where({ id }) + .and.where({ version }) .select(); - const exists = existingSchemas.some( - it => it.ref === ref && it.version === schema.version, - ); - if (!exists) { + if (!existingSchemas || existingSchemas.length === 0) { await this.db('fact_schemas').insert({ - ref, - version: schema.version, - schema: JSON.stringify(schema.schema), + id, + version, + entityTypes: entityTypes && entityTypes.join(','), + schema: JSON.stringify(schema), }); } } - async insertFacts(ref: string, facts: TechInsightFact[]): Promise { + async insertFacts(id: string, facts: TechInsightFact[]): Promise { if (facts.length === 0) return; - const currentSchema = await this.getLatestSchema(ref); + const currentSchema = await this.getLatestSchema(id); const factRows = facts.map(it => { - const { namespace, name, kind } = it.entity; return { - ref: ref, + id, version: currentSchema.version, - entity: `${namespace}/${kind}/${name}`.toLocaleLowerCase('en-US'), + entity: stringifyEntityRef(it.entity), facts: JSON.stringify(it.facts), ...(it.timestamp && { timestamp: it.timestamp.toJSDate() }), }; @@ -99,36 +101,36 @@ export class TechInsightsDatabase implements TechInsightsStore { }); } - async getLatestFactsForRefs( - refs: string[], + async getLatestFactsByIds( + ids: string[], entityTriplet: string, - ): Promise<{ [p: string]: FlatTechInsightFact }> { + ): Promise<{ [factId: string]: FlatTechInsightFact }> { const results = await this.db('facts') .where({ entity: entityTriplet }) - .and.whereIn('ref', refs) + .and.whereIn('id', ids) .join( this.db('facts') .max('timestamp') - .column('ref as subRef') - .groupBy('ref') + .column('id as subId') + .groupBy('id') .as('subQ'), - 'facts.ref', - 'subQ.subRef', + 'facts.id', + 'subQ.subId', ); return this.dbFactRowsToTechInsightFacts(results); } - async getFactsBetweenTimestampsForRefs( - refs: string[], + async getFactsBetweenTimestampsByIds( + ids: string[], entityTriplet: string, startDateTime: DateTime, endDateTime: DateTime, ): Promise<{ - [p: string]: FlatTechInsightFact[]; + [factId: string]: FlatTechInsightFact[]; }> { const results = await this.db('facts') .where({ entity: entityTriplet }) - .and.whereIn('ref', refs) + .and.whereIn('id', ids) .and.whereBetween('timestamp', [ startDateTime.toISO(), endDateTime.toISO(), @@ -136,31 +138,31 @@ export class TechInsightsDatabase implements TechInsightsStore { return groupBy( results.map(it => { - const [namespace, kind, name] = it.entity.split('/'); + const { namespace, kind, name } = parseEntityName(it.entity); const timestamp = typeof it.timestamp === 'string' ? DateTime.fromISO(it.timestamp) : DateTime.fromJSDate(it.timestamp); return { - ref: it.ref, + id: it.id, entity: { namespace, kind, name }, timestamp, version: it.version, facts: JSON.parse(it.facts), }; }), - 'ref', + 'id', ); } - private async getLatestSchema(ref: string): Promise { + private async getLatestSchema(id: string): Promise { const existingSchemas = await this.db('fact_schemas') - .where({ ref }) + .where({ id }) .orderBy('id', 'desc') .select(); if (existingSchemas.length < 1) { - this.logger.warn(`No schema found for ${ref}. `); - throw new Error(`No schema found for ${ref}. `); + this.logger.warn(`No schema found for ${id}. `); + throw new Error(`No schema found for ${id}. `); } const sorted = rsort(existingSchemas.map(it => it.version)); return existingSchemas.find(it => it.version === sorted[0])!!; @@ -168,15 +170,15 @@ export class TechInsightsDatabase implements TechInsightsStore { private dbFactRowsToTechInsightFacts(rows: RawDbFactRow[]) { return rows.reduce((acc, it) => { - const [namespace, kind, name] = it.entity.split('/'); + const { namespace, kind, name } = parseEntityName(it.entity); const timestamp = typeof it.timestamp === 'string' ? DateTime.fromISO(it.timestamp) : DateTime.fromJSDate(it.timestamp); return { ...acc, - [it.ref]: { - ref: it.ref, + [it.id]: { + id: it.id, entity: { namespace, kind, name }, timestamp, version: it.version, diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index 1433722e6c..075f67ccfe 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DefaultTechInsightsBuilder } from './DefaultTechInsightsBuilder'; +import { buildTechInsightsContext } from './techInsightsContextBuilder'; import { createRouter } from './router'; import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; @@ -27,14 +27,14 @@ import { Knex } from 'knex'; describe('Tech Insights router tests', () => { let app: express.Express; - const latestFactsForRefsMock = jest.fn(); - const factsBetweenTimestampsForRefsMock = jest.fn(); + const latestFactsByIdsMock = jest.fn(); + const factsBetweenTimestampsByIdsMock = jest.fn(); const latestSchemasMock = jest.fn(); const mockPersistenceContext: PersistenceContext = { techInsightsStore: { - getLatestFactsForRefs: latestFactsForRefsMock, - getFactsBetweenTimestampsForRefs: factsBetweenTimestampsForRefsMock, + getLatestFactsByIds: latestFactsByIdsMock, + getFactsBetweenTimestampsByIds: factsBetweenTimestampsByIdsMock, getLatestSchemas: latestSchemasMock, } as unknown as TechInsightsStore, }; @@ -44,7 +44,7 @@ describe('Tech Insights router tests', () => { }); beforeAll(async () => { - const techInsightsContext = await new DefaultTechInsightsBuilder({ + const techInsightsContext = await buildTechInsightsContext({ database: { getClient: () => { return Promise.resolve({ @@ -61,7 +61,7 @@ describe('Tech Insights router tests', () => { getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), }, - }).build(); + }); const router = await createRouter({ logger: getVoidLogger(), @@ -80,32 +80,36 @@ describe('Tech Insights router tests', () => { it('should not contain check endpoints when checker not present', async () => { await request(app).get('/checks').expect(404); - await request(app).get('/checks/a/a/a').expect(404); + await request(app).post('/checks/a/a/a').expect(404); }); - it('should parse be able to parse ref request params for fact retrieval', async () => { + it('should be able to parse id request params for fact retrieval', async () => { await request(app) - .get('/facts/latest/a/a/a') - .query({ refs: ['firstref', 'secondref'] }) + .get('/facts/latest') + .query({ + entity: 'a:a/a', + ids: ['firstId', 'secondId'], + }) .expect(200); - expect(latestFactsForRefsMock).toHaveBeenCalledWith( - ['firstref', 'secondref'], - 'a/a/a', + expect(latestFactsByIdsMock).toHaveBeenCalledWith( + ['firstId', 'secondId'], + 'a:a/a', ); }); - it('should parse be able to parse datetime request params for fact retrieval', async () => { + it('should be able to parse datetime request params for fact retrieval', async () => { await request(app) - .get('/facts/range/a/a/a') + .get('/facts/range') .query({ - refs: ['firstref', 'secondref'], + entity: 'a:a/a', + ids: ['firstId', 'secondId'], startDatetime: '2021-12-12T12:12:12', endDatetime: '2022-11-11T11:11:11', }) .expect(200); - expect(factsBetweenTimestampsForRefsMock).toHaveBeenCalledWith( - ['firstref', 'secondref'], - 'a/a/a', + expect(factsBetweenTimestampsByIdsMock).toHaveBeenCalledWith( + ['firstId', 'secondId'], + 'a:a/a', DateTime.fromISO('2021-12-12T12:12:12.000+00:00'), DateTime.fromISO('2022-11-11T11:11:11.000+00:00'), ); @@ -113,13 +117,14 @@ describe('Tech Insights router tests', () => { it('should respond gracefully on parsing errors', async () => { await request(app) - .get('/facts/range/a/a/a') + .get('/facts/range') .query({ - refs: ['firstref', 'secondref'], + entity: 'a:a/a', + ids: ['firstId', 'secondId'], startDatetime: '2021-12-1222T12:12:12', endDatetime: '2022-1122-11T11:11:11', }) .expect(422); - expect(latestFactsForRefsMock).toHaveBeenCalledTimes(0); + expect(latestFactsByIdsMock).toHaveBeenCalledTimes(0); }); }); diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index cc41868cd8..d02796117b 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -25,6 +25,11 @@ import { import { Logger } from 'winston'; import { DateTime } from 'luxon'; import { PersistenceContext } from './persistence/DatabaseManager'; +import { + EntityRef, + parseEntityName, + stringifyEntityRef, +} from '@backstage/catalog-model'; /** * @public @@ -92,7 +97,7 @@ export async function createRouter< }); } const { checks }: { checks: string[] } = req.body; - const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; + const entityTriplet = stringifyEntityRef({ namespace, kind, name }); const checkResult = await factChecker.runChecks(entityTriplet, checks); return res.send(checkResult); } catch (e) { @@ -106,22 +111,33 @@ export async function createRouter< } router.get('/fact-schemas', async (req, res) => { - const refs = req.query.refs as string[]; - return res.send(await techInsightsStore.getLatestSchemas(refs)); + const ids = req.query.ids as string[]; + return res.send(await techInsightsStore.getLatestSchemas(ids)); }); - router.get('/facts/latest/:namespace/:kind/:name', async (req, res) => { - const { namespace, kind, name } = req.params; - const refs = req.query.refs as string[]; - const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; + /** + * /facts/latest?entity=component:default/mycomponent&ids[]=factRetrieverId1&ids[]=factRetrieverId2 + */ + router.get('/facts/latest', async (req, res) => { + const { entity } = req.query; + const { namespace, kind, name } = parseEntityName(entity as EntityRef); + const ids = req.query.ids as string[]; return res.send( - await techInsightsStore.getLatestFactsForRefs(refs, entityTriplet), + await techInsightsStore.getLatestFactsByIds( + ids, + stringifyEntityRef({ namespace, kind, name }), + ), ); }); - router.get('/facts/range/:namespace/:kind/:name', async (req, res) => { - const { namespace, kind, name } = req.params; - const refs = req.query.refs as string[]; + /** + * /facts/latest?entity=component:default/mycomponent&startDateTime=2021-12-24T01:23:45&endDateTime=2021-12-31T23:59:59&ids[]=factRetrieverId1&ids[]=factRetrieverId2 + */ + router.get('/facts/range', async (req, res) => { + const { entity } = req.query; + const { namespace, kind, name } = parseEntityName(entity as EntityRef); + + const ids = req.query.ids as string[]; const startDatetime = DateTime.fromISO(req.query.startDatetime as string); const endDatetime = DateTime.fromISO(req.query.endDatetime as string); if (!startDatetime.isValid || !endDatetime.isValid) { @@ -131,10 +147,10 @@ export async function createRouter< value: !startDatetime.isValid ? startDatetime : endDatetime, }); } - const entityTriplet = `${namespace.toLowerCase()}/${kind.toLowerCase()}/${name.toLowerCase()}`; + const entityTriplet = stringifyEntityRef({ namespace, kind, name }); return res.send( - await techInsightsStore.getFactsBetweenTimestampsForRefs( - refs, + await techInsightsStore.getFactsBetweenTimestampsByIds( + ids, entityTriplet, startDatetime, endDatetime, diff --git a/plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts similarity index 63% rename from plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts rename to plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index 6c5b946c4c..bf30cdcf38 100644 --- a/plugins/tech-insights-backend/src/service/DefaultTechInsightsBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -80,70 +80,57 @@ export type TechInsightsContext< }; /** - * @public - * @typeParam CheckType - Type of the check for the fact checker this builder returns - * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + * Constructs needed persistence context, fact retriever engine + * and optionally fact checker implementations to be used in the tech insights module. * - * Default implementation of TechInsightsBuilder. + * @param options - Needed options to construct TechInsightsContext + * @returns TechInsightsContext with persistence implementations and optionally an implementation of a FactChecker */ -export class DefaultTechInsightsBuilder< +export const buildTechInsightsContext = async < CheckType extends TechInsightCheck, CheckResultType extends CheckResult, -> { - private readonly options: TechInsightsOptions; +>( + options: TechInsightsOptions, +): Promise> => { + const { + factRetrievers, + factCheckerFactory, + config, + discovery, + database, + logger, + } = options; - constructor(options: TechInsightsOptions) { - this.options = options; - } + const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); - /** - * Constructs needed persistence context, fact retriever engine - * and optionally fact checker implementations to be used in the tech insights module. - * - * @returns TechInsightsContext with persistence implementations and optionally an implementation of a FactChecker - */ - async build(): Promise> { - const { - factRetrievers, - factCheckerFactory, + const persistenceContext = await DatabaseManager.initializePersistenceContext( + await database.getClient(), + { logger }, + ); + + const factRetrieverEngine = await FactRetrieverEngine.create({ + repository: persistenceContext.techInsightsStore, + factRetrieverRegistry, + factRetrieverContext: { config, discovery, - database, logger, - } = this.options; + }, + }); - const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); - - const persistenceContext = - await DatabaseManager.initializePersistenceContext( - await database.getClient(), - { logger }, - ); - - const factRetrieverEngine = await FactRetrieverEngine.fromConfig({ - repository: persistenceContext.techInsightsStore, - factRetrieverRegistry, - factRetrieverContext: { - config, - discovery, - logger, - }, - }); - - factRetrieverEngine.schedule(); - - if (factCheckerFactory) { - const factChecker = factCheckerFactory.construct( - persistenceContext.techInsightsStore, - ); - return { - persistenceContext, - factChecker, - }; - } + factRetrieverEngine.schedule(); + if (factCheckerFactory) { + const factChecker = factCheckerFactory.construct( + persistenceContext.techInsightsStore, + ); return { persistenceContext, + factChecker, }; } -} + + return { + persistenceContext, + }; +}; diff --git a/plugins/tech-insights-common/CHANGELOG.md b/plugins/tech-insights-common/CHANGELOG.md deleted file mode 100644 index 17e0211ab0..0000000000 --- a/plugins/tech-insights-common/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @backstage/plugin-tech-insights-backend - -## 0.0.1 - -### Patch Changes - -- Initial implementation diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index db987546c3..e83ba18137 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -17,7 +17,7 @@ export interface BooleanCheckResult extends CheckResult { // @public export interface CheckResponse { description: string; - factRefs: string[]; + factIds: string[]; id: string; metadata?: Record; name: string; @@ -68,22 +68,23 @@ export interface FactCheckerFactory< // @public export type FactResponse = { - [key: string]: { - ref: string; + [id: string]: { + id: string; type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; description: string; value: number | string | boolean | DateTime | []; since?: string; metadata?: Record; - entityKinds: string[]; }; }; // @public export interface FactRetriever { + entityTypes?: string[]; handler: (ctx: FactRetrieverContext) => Promise; - ref: string; + id: string; schema: FactSchema; + version: string; } // @public @@ -101,30 +102,28 @@ export type FactRetrieverRegistration = { // @public export type FactSchema = { - version: string; - schema: FactValueDefinitions; -}; - -// @public -export type FactValueDefinitions = { - [key: string]: { + [name: string]: { type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; description: string; since?: string; metadata?: Record; - entityKinds: string[]; }; }; +// Warning: (ae-missing-release-tag) "FactSchemaDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type FactSchemaDefinition = Omit; + // @public export type FlatTechInsightFact = TechInsightFact & { - ref: string; + id: string; }; // @public export interface TechInsightCheck { description: string; - factRefs: string[]; + factIds: string[]; failureMetadata?: Record; id: string; name: string; @@ -151,14 +150,24 @@ export type TechInsightFact = { kind: string; name: string; }; - facts: Record; + facts: Record< + string, + | number + | string + | boolean + | DateTime + | number[] + | string[] + | boolean[] + | DateTime[] + >; timestamp?: DateTime; }; // @public export interface TechInsightsStore { - getFactsBetweenTimestampsForRefs( - refs: string[], + getFactsBetweenTimestampsByIds( + ids: string[], entity: string, startDateTime: DateTime, endDateTime: DateTime, @@ -166,15 +175,15 @@ export interface TechInsightsStore { [factRef: string]: FlatTechInsightFact[]; }>; // (undocumented) - getLatestFactsForRefs( - refs: string[], + getLatestFactsByIds( + ids: string[], entity: string, ): Promise<{ [factRef: string]: FlatTechInsightFact; }>; - getLatestSchemas(refs?: string[]): Promise; - insertFacts(ref: string, facts: TechInsightFact[]): Promise; - insertFactSchema(ref: string, schema: FactSchema): Promise; + getLatestSchemas(ids?: string[]): Promise; + insertFacts(id: string, facts: TechInsightFact[]): Promise; + insertFactSchema(schemaDefinition: FactSchemaDefinition): Promise; } // (No @packageDocumentation comment for this package) diff --git a/plugins/tech-insights-common/src/checks.ts b/plugins/tech-insights-common/src/checks.ts index 7b2c4c6b8f..90fa7ee34f 100644 --- a/plugins/tech-insights-common/src/checks.ts +++ b/plugins/tech-insights-common/src/checks.ts @@ -157,7 +157,7 @@ export interface TechInsightCheck { * * References the fact container, aka fact retriever itself which may or may not contain multiple individual facts and values */ - factRefs: string[]; + factIds: string[]; /** * Metadata to be returned in case a check has been successfully evaluated diff --git a/plugins/tech-insights-common/src/facts.ts b/plugins/tech-insights-common/src/facts.ts index 1ae507adf8..a46e7bd95d 100644 --- a/plugins/tech-insights-common/src/facts.ts +++ b/plugins/tech-insights-common/src/facts.ts @@ -42,7 +42,17 @@ export type TechInsightFact = { * * Key indicates fact name as it is defined in FactSchema */ - facts: Record; + facts: Record< + string, + | number + | string + | boolean + | DateTime + | number[] + | string[] + | boolean[] + | DateTime[] + >; /** * Optional timestamp value which can be used to override retrieval time of the fact row. @@ -62,7 +72,7 @@ export type FlatTechInsightFact = TechInsightFact & { /** * Reference and unique identifier of the fact row */ - ref: string; + id: string; }; /** @@ -73,8 +83,11 @@ export type FlatTechInsightFact = TechInsightFact & { * Used as part of a schema to validate, identify and generically construct usage implementations * of individual fact values in the system. */ -export type FactValueDefinitions = { - [key: string]: { +export type FactSchema = { + /** + * Name of the fact + */ + [name: string]: { /** * Type of the individual fact value * @@ -109,30 +122,9 @@ export type FactValueDefinitions = { * ``` */ metadata?: Record; - - /** - * A list of entity kind descriptors to indicate if this fact is valid for an entity kind - */ - entityKinds: string[]; }; }; -/** - * @public - * - * Container for FactSchema - */ -export type FactSchema = { - /** - * Semver string indicating the version of this schema - */ - version: string; - /** - * Actual schema definitions for this schema - */ - schema: FactValueDefinitions; -}; - /** * @public * @@ -159,7 +151,15 @@ export interface FactRetriever { * Used to identify and store individual facts returned from this retriever * and schemas defined by this retriever. */ - ref: string; + id: string; + + /** + * Semver string indicating the version of this fact retriever + * This version is used to determine if the schema this fact retriever matches the data this fact retriever provides. + * + * Should be incremented on changes to returned data from the handler or if the schema changes. + */ + version: string; /** * Handler function that needs to be implemented to retrieve fact values for entities. @@ -173,8 +173,20 @@ export interface FactRetriever { * A fact schema defining the shape of data returned from the handler method for each entity */ schema: FactSchema; + + /** + * An optional list of entity type descriptors to indicate if this fact retriever is valid for an entity type. + * If omitted, the retriever should apply to all entities. + * + * Should be defined as: + * ['component', 'group', 'user'] for top level items + * ['component:service', 'component:website'] for component types. + */ + entityTypes?: string[]; } +export type FactSchemaDefinition = Omit; + /** * @public * diff --git a/plugins/tech-insights-common/src/persistence.ts b/plugins/tech-insights-common/src/persistence.ts index 9ffb6a9a28..444e961008 100644 --- a/plugins/tech-insights-common/src/persistence.ts +++ b/plugins/tech-insights-common/src/persistence.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FactSchema, TechInsightFact, FlatTechInsightFact } from './facts'; +import { + FactSchema, + TechInsightFact, + FlatTechInsightFact, + FactSchemaDefinition, +} from './facts'; import { DateTime } from 'luxon'; /** @@ -28,34 +33,34 @@ export interface TechInsightsStore { * * Each row may contain multiple individual facts and values * - * @param ref - Unique identifier of the fact retriever these facts relate to + * @param id - Unique identifier of the fact retriever these facts relate to * @param facts - A collection of TechInsightFacts */ - insertFacts(ref: string, facts: TechInsightFact[]): Promise; + insertFacts(id: string, facts: TechInsightFact[]): Promise; /** - * @param refs - A collection of reference string to a fact row + * @param ids - A collection of fact row identifiers * @param entity - A string identifying an entity. In a format namespace/kind/name * * @returns - An object keyed by a fact reference and containing an individual TechInsightFact */ - getLatestFactsForRefs( - refs: string[], + getLatestFactsByIds( + ids: string[], entity: string, ): Promise<{ [factRef: string]: FlatTechInsightFact }>; /** * Retrieves fact values identified by fact row references for an individual entity. * - * @param refs - A collection of reference string to a fact row + * @param ids - A collection of fact row identifiers * @param entity - A string identifying an entity. In a format namespace/kind/name * @param startDateTime - DateTime object indicating start of the time frame * @param endDateTime - DateTime object indicating start of the time frame * * @returns - An object keyed by a fact reference and containing a collection of TechInsightFacts matching the time frame */ - getFactsBetweenTimestampsForRefs( - refs: string[], + getFactsBetweenTimestampsByIds( + ids: string[], entity: string, startDateTime: DateTime, endDateTime: DateTime, @@ -64,16 +69,15 @@ export interface TechInsightsStore { /** * Stores versioned fact schemas into data store * - * @param ref - Identifier of the fact schema. Reference to a fact retriever. - * @param schema - The actual schema to store + * @param schemaDefinition - FactSchemaDefinition containing id, version, schema and entityTypes. */ - insertFactSchema(ref: string, schema: FactSchema): Promise; + insertFactSchema(schemaDefinition: FactSchemaDefinition): Promise; /** * Retrieves latest versions (as defined by semver) of fact schemas from the data store. * - * @param refs - Collection of refs to return. If omitted, all Schemas should be returned. + * @param ids - Collection of ids to return. If omitted, all Schemas should be returned. * @returns - A collection of schemas */ - getLatestSchemas(refs?: string[]): Promise; + getLatestSchemas(ids?: string[]): Promise; } diff --git a/plugins/tech-insights-common/src/responses.ts b/plugins/tech-insights-common/src/responses.ts index 0cd1d0a2e8..c3dee1df48 100644 --- a/plugins/tech-insights-common/src/responses.ts +++ b/plugins/tech-insights-common/src/responses.ts @@ -44,7 +44,7 @@ export interface CheckResponse { /** * A collection of references to fact rows used to run this checks against */ - factRefs: string[]; + factIds: string[]; /** * Metadata related to a check. @@ -62,11 +62,11 @@ export interface CheckResponse { * Keyed by the name of the fact */ export type FactResponse = { - [key: string]: { + [id: string]: { /** * Reference and unique identifier of the fact row */ - ref: string; + id: string; /** * Type of the individual fact value * @@ -97,10 +97,5 @@ export type FactResponse = { * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined */ metadata?: Record; - - /** - * A list of entity kind descriptors to indicate if this fact is valid for an entity kind - */ - entityKinds: string[]; }; }; From 2b3e959ef47e37b2e944356e4c26bb12ae451e97 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 28 Oct 2021 16:20:44 +0200 Subject: [PATCH 215/245] Modifications after PR review * Change entity filter to be an actual entity filter instead of a list of kinds or types * Modify/simplify types a little bit * Split common type libs to one for node and one isomorphic * Remove unnecessary items from FactChecker interface to simplify execution loop. Needs still matching README.md changes. Signed-off-by: Jussi Hallila --- packages/backend/package.json | 2 +- packages/backend/src/plugins/techInsights.ts | 72 +++++---- .../api-report.md | 16 +- .../package.json | 6 +- .../src/service/CheckRegistry.ts | 2 +- .../JsonRulesEngineFactChecker.test.ts | 2 +- .../src/service/JsonRulesEngineFactChecker.ts | 42 ++--- .../src/types.ts | 2 +- plugins/tech-insights-backend/README.md | 39 +++-- plugins/tech-insights-backend/api-report.md | 19 ++- .../migrations/202109061111_fact_schemas.js | 4 +- plugins/tech-insights-backend/package.json | 1 + plugins/tech-insights-backend/src/index.ts | 1 + .../service/fact/FactRetrieverEngine.test.ts | 8 +- .../src/service/fact/FactRetrieverEngine.ts | 2 +- .../src/service/fact/FactRetrieverRegistry.ts | 2 +- .../src/service/fact/createFactRetriever.ts | 49 ++++++ .../service/persistence/DatabaseManager.ts | 2 +- .../persistence/TechInsightsDatabase.test.ts | 18 +-- .../persistence/TechInsightsDatabase.ts | 10 +- .../src/service/router.test.ts | 2 +- .../src/service/router.ts | 5 +- .../src/service/techInsightsContextBuilder.ts | 6 +- plugins/tech-insights-common/README.md | 2 +- plugins/tech-insights-common/api-report.md | 147 ------------------ plugins/tech-insights-common/package.json | 5 +- plugins/tech-insights-common/src/index.ts | 112 ++++++++++++- plugins/tech-insights-common/src/responses.ts | 101 ------------ plugins/tech-insights-node/.eslintrc.js | 3 + plugins/tech-insights-node/README.md | 3 + plugins/tech-insights-node/package.json | 46 ++++++ .../src/checks.ts | 54 +------ .../src/facts.ts | 12 +- plugins/tech-insights-node/src/index.test.ts | 24 +++ plugins/tech-insights-node/src/index.ts | 19 +++ .../src/persistence.ts | 0 .../src/setupTests.ts | 0 37 files changed, 393 insertions(+), 447 deletions(-) create mode 100644 plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts delete mode 100644 plugins/tech-insights-common/src/responses.ts create mode 100644 plugins/tech-insights-node/.eslintrc.js create mode 100644 plugins/tech-insights-node/README.md create mode 100644 plugins/tech-insights-node/package.json rename plugins/{tech-insights-common => tech-insights-node}/src/checks.ts (77%) rename plugins/{tech-insights-common => tech-insights-node}/src/facts.ts (92%) create mode 100644 plugins/tech-insights-node/src/index.test.ts create mode 100644 plugins/tech-insights-node/src/index.ts rename plugins/{tech-insights-common => tech-insights-node}/src/persistence.ts (100%) rename plugins/{tech-insights-common => tech-insights-node}/src/setupTests.ts (100%) diff --git a/packages/backend/package.json b/packages/backend/package.json index a56b302954..f1d58252a4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -49,7 +49,7 @@ "@backstage/plugin-search-backend-module-pg": "^0.2.1", "@backstage/plugin-techdocs-backend": "^0.10.5", "@backstage/plugin-tech-insights-backend": "^0.1.0", - "@backstage/plugin-tech-insights-common": "^0.1.0", + "@backstage/plugin-tech-insights-node": "^0.1.0", "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.0", "@backstage/plugin-todo-backend": "^0.1.13", "@gitbeaker/node": "^30.2.0", diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index d51c0c7575..9927d98f87 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -16,14 +16,15 @@ import { createRouter, buildTechInsightsContext, + createFactRetrieverRegistration, } from '@backstage/plugin-tech-insights-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import { - JSON_RULE_ENGINE_CHECK_TYPE, - JsonRulesEngineFactCheckerFactory, -} from '@backstage/plugin-tech-insights-backend-module-jsonfc'; import { CatalogClient } from '@backstage/catalog-client'; +import { + JsonRulesEngineFactCheckerFactory, + JSON_RULE_ENGINE_CHECK_TYPE, +} from '@backstage/plugin-tech-insights-backend-module-jsonfc'; export default async function createPlugin({ logger, @@ -37,41 +38,38 @@ export default async function createPlugin({ database, discovery, factRetrievers: [ - { - cadence: '1 1 1 * *', // At 01:01 on day-of-month 1. - factRetriever: { - id: 'testRetriever', - version: '1.1.1', - entityTypes: ['component'], - schema: { - examplenumberfact: { - type: 'integer', - description: '', - }, - }, - handler: async _ctx => { - const catalogClient = new CatalogClient({ - discoveryApi: discovery, - }); - const entities = await catalogClient.getEntities(); - - return Promise.resolve( - entities.items.map(it => { - return { - entity: { - namespace: it.metadata.namespace!!, - kind: it.kind, - name: it.metadata.name, - }, - facts: { - examplenumberfact: 2, - }, - }; - }), - ); + createFactRetrieverRegistration('* * * * *', { + id: 'testRetriever', + version: '1.1.2', + entityFilter: [{ kind: 'component' }], // EntityFilter to be used in the future (creating checks, graphs etc.) to figure out which entities this fact retrieves data for. + schema: { + examplenumberfact: { + type: 'integer', + description: 'Example fact returning a number', }, }, - }, + handler: async _ctx => { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities(); + + return Promise.resolve( + entities.items.map(it => { + return { + entity: { + namespace: it.metadata.namespace!!, + kind: it.kind, + name: it.metadata.name, + }, + facts: { + examplenumberfact: 2, + }, + }; + }), + ); + }, + }), ], factCheckerFactory: new JsonRulesEngineFactCheckerFactory({ checks: [ diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index 3d214566c5..7093fbfc2b 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -5,12 +5,12 @@ ```ts import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; import { CheckResponse } from '@backstage/plugin-tech-insights-common'; -import { CheckValidationResponse } from '@backstage/plugin-tech-insights-common'; -import { FactChecker } from '@backstage/plugin-tech-insights-common'; +import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node'; +import { FactChecker } from '@backstage/plugin-tech-insights-node'; import { Logger as Logger_2 } from 'winston'; -import { TechInsightCheck } from '@backstage/plugin-tech-insights-common'; -import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-common'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; +import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-node'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { TopLevelCondition } from 'json-rules-engine'; // @public (undocumented) @@ -52,13 +52,11 @@ export class JsonRulesEngineFactChecker checkRegistry, }: JsonRulesEngineFactCheckerOptions); // (undocumented) - addCheck(check: TechInsightJsonRuleCheck): Promise; - // (undocumented) getChecks(): Promise; // (undocumented) runChecks( entity: string, - checks: string[], + checks?: string[], ): Promise; // (undocumented) validate(check: TechInsightJsonRuleCheck): Promise; @@ -79,7 +77,7 @@ export class JsonRulesEngineFactCheckerFactory { export type JsonRulesEngineFactCheckerFactoryOptions = { checks: TechInsightJsonRuleCheck[]; logger: Logger_2; - checkRegistry?: TechInsightCheckRegistry; + checkRegistry?: TechInsightCheckRegistry; }; // @public diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index c11f20ee38..a1db34a97e 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,8 +1,8 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", "version": "0.1.0", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", + "main": "src/index.ts", + "types": "src/index.ts", "license": "Apache-2.0", "private": false, "publishConfig": { @@ -35,11 +35,11 @@ "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.1", "@backstage/plugin-tech-insights-common": "^0.1.0", + "@backstage/plugin-tech-insights-node": "^0.1.0", "ajv": "^7.0.3", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", "luxon": "^2.0.2", - "node-cron": "^3.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts index 016fb05b31..dec84a8634 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts @@ -18,7 +18,7 @@ import { ConflictError, NotFoundError } from '@backstage/errors'; import { TechInsightCheck, TechInsightCheckRegistry, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; export class DefaultCheckRegistry implements TechInsightCheckRegistry diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts index a31ae90c70..77b3e6c8b1 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -17,7 +17,7 @@ import { TechInsightCheckRegistry, TechInsightsStore, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; import { JSON_RULE_ENGINE_CHECK_TYPE, JsonRulesEngineFactCheckerFactory, diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts index ab90a3d828..ae1f18a373 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -17,13 +17,12 @@ import { JsonRuleBooleanCheckResult, TechInsightJsonRuleCheck } from '../types'; import { FactChecker, - FactResponse, TechInsightCheckRegistry, FlatTechInsightFact, TechInsightsStore, CheckValidationResponse, - CheckValidationError, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; +import { FactResponse } from '@backstage/plugin-tech-insights-common'; import { Engine, EngineResult, TopLevelCondition } from 'json-rules-engine'; import { DefaultCheckRegistry } from './CheckRegistry'; import { Logger } from 'winston'; @@ -71,15 +70,19 @@ export class JsonRulesEngineFactChecker this.repository = repository; this.logger = logger; checks.forEach(check => this.validate(check)); - this.checkRegistry = checkRegistry ?? new DefaultCheckRegistry(checks); + this.checkRegistry = + checkRegistry ?? + new DefaultCheckRegistry(checks); } async runChecks( entity: string, - checks: string[], + checks?: string[], ): Promise { const engine = new Engine(); - const techInsightChecks = await this.checkRegistry.getAll(checks); + const techInsightChecks = checks + ? await this.checkRegistry.getAll(checks) + : await this.checkRegistry.list(); const factIds = techInsightChecks.flatMap(it => it.factIds); const facts = await this.repository.getLatestFactsByIds(factIds, entity); techInsightChecks.forEach(techInsightCheck => { @@ -127,7 +130,7 @@ export class JsonRulesEngineFactChecker return { valid: false, message: msg, - errors: validator.errors, + errors: validator.errors ? validator.errors : undefined, }; } @@ -166,21 +169,6 @@ export class JsonRulesEngineFactChecker return this.checkRegistry.list(); } - async addCheck( - check: TechInsightJsonRuleCheck, - ): Promise { - const checkValidationResponse = await this.validate(check); - if (!checkValidationResponse.valid) { - const msg = `Check validation failed when adding check ${check.name} to check registry.`; - this.logger.warn(msg); - throw new CheckValidationError({ - message: checkValidationResponse.message || msg, - errors: checkValidationResponse.errors, - }); - } - return await this.checkRegistry.register(check); - } - private retrieveIndividualFactReferences( condition: TopLevelCondition | { fact: string }, ): string[] { @@ -255,8 +243,10 @@ export class JsonRulesEngineFactChecker }; if ('toJSON' in result) { - // Results serialize "wrong" since the objects are creating their own serialization implementations - // 'toJSON' should always be present in the result object but it is missing from the types + // Results from json-rules-engine serialize "wrong" since the objects are creating their own serialization implementations. + // 'toJSON' should always be present in the result object but it is missing from the types. + // Parsing the stringified representation into a plain object here to be able to serialize it later + // along with other items present in the returned response. const rule = JSON.parse(result.toJSON()); return { ...returnable, rule: pick(rule, ['conditions']) }; } @@ -312,7 +302,7 @@ export class JsonRulesEngineFactChecker export type JsonRulesEngineFactCheckerFactoryOptions = { checks: TechInsightJsonRuleCheck[]; logger: Logger; - checkRegistry?: TechInsightCheckRegistry; + checkRegistry?: TechInsightCheckRegistry; }; /** @@ -325,7 +315,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = { export class JsonRulesEngineFactCheckerFactory { private readonly checks: TechInsightJsonRuleCheck[]; private readonly logger: Logger; - private readonly checkRegistry?: TechInsightCheckRegistry; + private readonly checkRegistry?: TechInsightCheckRegistry; constructor({ checks, diff --git a/plugins/tech-insights-backend-module-jsonfc/src/types.ts b/plugins/tech-insights-backend-module-jsonfc/src/types.ts index 23d5849eda..592fee248d 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/types.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/types.ts @@ -14,10 +14,10 @@ * limitations under the License. */ import { TopLevelCondition } from 'json-rules-engine'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; import { BooleanCheckResult, CheckResponse, - TechInsightCheck, } from '@backstage/plugin-tech-insights-common'; /** diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 32265c1ced..2de16b65b0 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -77,16 +77,18 @@ you will not have any fact retrievers present in your application. To have the i To create factRetrieverRegistration you need to implement `FactRetriever` interface defined in `@backstage/plugin-tech-insights-common` package. After you have implemented this interface you can wrap that into a registration object like follows: ```ts -const myFactRetriever: FactRetriever = { +import { createFactRetrieverRegistration } from './createFactRetriever'; + +const myFactRetriever = { /** * snip */ }; -const myFactRetrieverRegistration = { - cadence: '1 * 3 * * ', // On the first minute of the third day of the month - factRetriever: myFactRetriever, -}; +const myFactRetrieverRegistration = createFactRetrieverRegistration( + '1 * 3 * * ', // On the first minute of the third day of the month + myFactRetriever, +); ``` Then you can modify the example `techInsights.ts` file shown above like this: @@ -104,28 +106,25 @@ discovery, ### Creating Fact Retrievers -A Fact Retriever consist of three parts: +A Fact Retriever consist of four parts: -1. `ref` - unique identifier of a fact retriever -2. `schema` - A versioned schema defining the shape of data a fact retriever returns -3. `handler` - An asynchronous function handling the logic of retrieving and returning facts for an entity +1. `id` - unique identifier of a fact retriever +2. `version`: A semver string indicating the current version of the schema and the handler +3. `schema` - A versioned schema defining the shape of data a fact retriever returns +4. `handler` - An asynchronous function handling the logic of retrieving and returning facts for an entity An example implementation of a FactRetriever could for example be as follows: ```ts const myFactRetriever: FactRetriever = { - ref: 'documentation-number-factretriever', // unique ref, identifier of the fact retriever + id: 'documentation-number-factretriever', // unique identifier of the fact retriever + version: '0.1.1', // SemVer version number of this fact retriever schema. This should be incremented if the implementation changes schema: { - version: '0.1.1', // SemVer version number of this fact retriever schema. This should be incremented if the implementation changes - - // the actual schema - schema: { - // Name/identifier of an individual fact that this retriever returns - examplenumberfact: { - type: 'integer', // Type of the fact - description: 'A fact of a number', // Description of the fact - entityTypes: ['component'], // An array of entity kinds that this fact is applicable to - }, + // Name/identifier of an individual fact that this retriever returns + examplenumberfact: { + type: 'integer', // Type of the fact + description: 'A fact of a number', // Description of the fact + entityTypes: ['component'], // An array of entity kinds that this fact is applicable to }, }, handler: async ctx => { diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 6bd161c092..826789dd1d 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -6,17 +6,16 @@ import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; import express from 'express'; -import { FactChecker } from '@backstage/plugin-tech-insights-common'; -import { FactCheckerFactory } from '@backstage/plugin-tech-insights-common'; -import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-common'; +import { FactChecker } from '@backstage/plugin-tech-insights-node'; +import { FactCheckerFactory } from '@backstage/plugin-tech-insights-node'; +import { FactRetriever } from '@backstage/plugin-tech-insights-node'; +import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node'; import { Logger as Logger_2 } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { TechInsightCheck } from '@backstage/plugin-tech-insights-common'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; -// Warning: (ae-missing-release-tag) "buildTechInsightsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const buildTechInsightsContext: < CheckType extends TechInsightCheck, @@ -25,6 +24,12 @@ export const buildTechInsightsContext: < options: TechInsightsOptions, ) => Promise>; +// @public +export function createFactRetrieverRegistration( + cadence: string, + factRetriever: FactRetriever, +): FactRetrieverRegistration; + // @public export function createRouter< CheckType extends TechInsightCheck, diff --git a/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js index 64f6c11e83..6094daa4b7 100644 --- a/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js +++ b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js @@ -33,10 +33,10 @@ exports.up = async function up(knex) { .notNullable() .comment('SemVer string defining the version of schema.'); table - .string('entityTypes') + .string('entityFilter') .nullable() .comment( - 'A comma separated collection of entity kinds the fact retriever providing this schema affects. Defaults to null, which means all entity kinds.', + 'A serialized entity filter object used to determine which entities this schema is applicable to.', ); table .text('schema') diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index fba5daae45..cf1e1d1c6f 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -37,6 +37,7 @@ "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.1", "@backstage/plugin-tech-insights-common": "^0.1.0", + "@backstage/plugin-tech-insights-node": "^0.1.0", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", diff --git a/plugins/tech-insights-backend/src/index.ts b/plugins/tech-insights-backend/src/index.ts index fd7f3ccd53..2a74bc6b88 100644 --- a/plugins/tech-insights-backend/src/index.ts +++ b/plugins/tech-insights-backend/src/index.ts @@ -24,3 +24,4 @@ export type { } from './service/techInsightsContextBuilder'; export type { PersistenceContext } from './service/persistence/DatabaseManager'; +export { createFactRetrieverRegistration } from './service/fact/createFactRetriever'; diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index ef53ae8412..4dc002465b 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -19,7 +19,7 @@ import { FactSchemaDefinition, TechInsightFact, TechInsightsStore, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistry } from './FactRetrieverRegistry'; import { FactRetrieverEngine } from './FactRetrieverEngine'; import { getVoidLogger } from '@backstage/backend-common'; @@ -37,7 +37,7 @@ jest.mock('node-cron', () => { const testFactRetriever: FactRetriever = { id: 'test-factretriever', version: '0.0.1', - entityTypes: ['component'], + entityFilter: [{ kind: 'component' }], schema: { testnumberfact: { type: 'integer', @@ -101,10 +101,10 @@ describe('FactRetrieverEngine', () => { }; it('Should update fact retriever schemas on initialization', async () => { - factSchemaAssertionCallback = ({ id, schema, version, entityTypes }) => { + factSchemaAssertionCallback = ({ id, schema, version, entityFilter }) => { expect(id).toEqual('test-factretriever'); expect(version).toEqual('0.0.1'); - expect(entityTypes).toEqual(['component']); + expect(entityFilter).toEqual([{ kind: 'component' }]); expect(schema).toEqual({ testnumberfact: { type: 'integer', diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts index 6dc3396819..aa2207cca8 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -17,7 +17,7 @@ import { FactRetriever, FactRetrieverContext, TechInsightsStore, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistry } from './FactRetrieverRegistry'; import { schedule, validate, ScheduledTask } from 'node-cron'; import { Logger } from 'winston'; diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts index 59d1483720..719f8bf504 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -18,7 +18,7 @@ import { FactRetriever, FactRetrieverRegistration, FactSchema, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; import { ConflictError, NotFoundError } from '@backstage/errors'; export class FactRetrieverRegistry { diff --git a/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts new file mode 100644 index 0000000000..b744d3b826 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts @@ -0,0 +1,49 @@ +/* + * 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 { + FactRetriever, + FactRetrieverRegistration, +} from '@backstage/plugin-tech-insights-node'; + +/** + * @public + * + * A helper function to construct fact retriever registrations. + * + * Cron expressions help: + * ┌────────────── second (optional) + # │ ┌──────────── minute + # │ │ ┌────────── hour + # │ │ │ ┌──────── day of month + # │ │ │ │ ┌────── month + # │ │ │ │ │ ┌──── day of week + # │ │ │ │ │ │ + # │ │ │ │ │ │ + # * * * * * * + * + * + * @param cadence - cron expression to indicate when the fact retriever should be triggered + * @param factRetriever - Implementation of fact retriever consisting of at least id, version, schema and handler + */ +export function createFactRetrieverRegistration( + cadence: string, + factRetriever: FactRetriever, +): FactRetrieverRegistration { + return { + cadence, + factRetriever, + }; +} diff --git a/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts b/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts index 75fb313f80..126c253fa5 100644 --- a/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts +++ b/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts @@ -18,7 +18,7 @@ import knexFactory, { Knex } from 'knex'; import { Logger } from 'winston'; import { v4 as uuidv4 } from 'uuid'; import { TechInsightsDatabase } from './TechInsightsDatabase'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; const migrationsDir = resolvePackagePath( '@backstage/plugin-tech-insights-backend', diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts index 44a376d5da..85d6098ce3 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts @@ -15,14 +15,14 @@ */ import { DatabaseManager } from './DatabaseManager'; import { DateTime, Duration } from 'luxon'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { Knex } from 'knex'; const factSchemas = [ { id: 'test-fact', version: '0.0.1-test', - entityTypes: ['component'], + entityFilter: JSON.stringify([{ kind: 'component' }]), schema: JSON.stringify({ testNumberFact: { type: 'integer', @@ -35,7 +35,7 @@ const additionalFactSchemas = [ { id: 'test-fact', version: '1.2.1-test', - entityTypes: ['component'], + entityFilter: JSON.stringify([{ kind: 'component' }]), schema: JSON.stringify({ testNumberFact: { type: 'integer', @@ -50,7 +50,7 @@ const additionalFactSchemas = [ { id: 'test-fact', version: '1.1.1-test', - entityTypes: ['component'], + entityFilter: JSON.stringify([{ kind: 'component' }]), schema: JSON.stringify({ testStringFact: { type: 'string', @@ -63,7 +63,7 @@ const additionalFactSchemas = [ const secondSchema = { id: 'second-test-fact', version: '0.0.1-test', - entityTypes: ['service'], + entityFilter: JSON.stringify([{ kind: 'service' }]), schema: JSON.stringify({ testStringFact: { type: 'string', @@ -137,7 +137,7 @@ describe('Tech Insights database', () => { expect(schemas[0]).toMatchObject({ id: 'test-fact', version: '0.0.1-test', - entityTypes: ['component'], + entityFilter: [{ kind: 'component' }], testNumberFact: { type: 'integer', description: 'Test fact with a number type', @@ -152,7 +152,7 @@ describe('Tech Insights database', () => { expect(schemas[0]).toMatchObject({ id: 'test-fact', version: '1.2.1-test', - entityTypes: ['component'], + entityFilter: [{ kind: 'component' }], testNumberFact: { type: 'integer', description: 'Test fact with a number type', @@ -177,7 +177,7 @@ describe('Tech Insights database', () => { expect(schemas[0]).toMatchObject({ id: 'test-fact', version: '1.2.1-test', - entityTypes: ['component'], + entityFilter: [{ kind: 'component' }], testNumberFact: { type: 'integer', description: 'Test fact with a number type', @@ -190,7 +190,7 @@ describe('Tech Insights database', () => { expect(schemas[1]).toMatchObject({ id: 'second', version: '0.0.1-test', - entityTypes: ['service'], + entityFilter: [{ kind: 'service' }], testStringFact: { type: 'string', description: 'Test fact with a string type', diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts index af45539b32..31a2997af5 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -20,7 +20,7 @@ import { FlatTechInsightFact, TechInsightsStore, FactSchemaDefinition, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; import { rsort } from 'semver'; import { groupBy, omit } from 'lodash'; import { DateTime } from 'luxon'; @@ -39,7 +39,7 @@ type RawDbFactSchemaRow = { id: string; version: string; schema: string; - entityTypes?: string; + entityFilter?: string; }; export class TechInsightsDatabase implements TechInsightsStore { @@ -63,12 +63,12 @@ export class TechInsightsDatabase implements TechInsightsStore { .map((it: RawDbFactSchemaRow) => ({ ...omit(it, 'schema'), ...JSON.parse(it.schema), - entityTypes: it.entityTypes ? it.entityTypes.split(',') : [], + entityFilter: it.entityFilter ? JSON.parse(it.entityFilter) : null, })); } async insertFactSchema(schemaDefinition: FactSchemaDefinition) { - const { id, version, schema, entityTypes } = schemaDefinition; + const { id, version, schema, entityFilter } = schemaDefinition; const existingSchemas = await this.db('fact_schemas') .where({ id }) .and.where({ version }) @@ -78,7 +78,7 @@ export class TechInsightsDatabase implements TechInsightsStore { await this.db('fact_schemas').insert({ id, version, - entityTypes: entityTypes && entityTypes.join(','), + entityFilter: entityFilter ? JSON.stringify(entityFilter) : undefined, schema: JSON.stringify(schema), }); } diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index 075f67ccfe..8a7b6bdbb3 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -20,7 +20,7 @@ import { ConfigReader } from '@backstage/config'; import request from 'supertest'; import express from 'express'; import { PersistenceContext } from './persistence/DatabaseManager'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-common'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { DateTime } from 'luxon'; import { Knex } from 'knex'; diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index d02796117b..752534928f 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -20,8 +20,9 @@ import { Config } from '@backstage/config'; import { FactChecker, TechInsightCheck, - CheckResult, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; + +import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Logger } from 'winston'; import { DateTime } from 'luxon'; import { PersistenceContext } from './persistence/DatabaseManager'; diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index bf30cdcf38..7305f1778c 100644 --- a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -23,16 +23,16 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { - CheckResult, FactChecker, FactCheckerFactory, FactRetrieverRegistration, TechInsightCheck, -} from '@backstage/plugin-tech-insights-common'; +} from '@backstage/plugin-tech-insights-node'; import { DatabaseManager, PersistenceContext, } from './persistence/DatabaseManager'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; /** * @public @@ -80,6 +80,8 @@ export type TechInsightsContext< }; /** + * @public + * * Constructs needed persistence context, fact retriever engine * and optionally fact checker implementations to be used in the tech insights module. * diff --git a/plugins/tech-insights-common/README.md b/plugins/tech-insights-common/README.md index 651c0e969d..af14ef0e0d 100644 --- a/plugins/tech-insights-common/README.md +++ b/plugins/tech-insights-common/README.md @@ -1,3 +1,3 @@ # Tech Insights Common -Common types and functionalities for tech insights, to be shared between tech-insights-backend and individual implementations of FactRetrievers, FactCheckers and their persistence options. +Common types and functionalities for tech insights shared in an isomorphic manner between BE and FE implementations. diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index e83ba18137..ddb38cfa85 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -3,10 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; -import { Logger as Logger_2 } from 'winston'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public export interface BooleanCheckResult extends CheckResult { @@ -30,42 +27,6 @@ export type CheckResult = { check: CheckResponse; }; -// @public -export class CheckValidationError extends Error { - constructor({ message, errors }: { message: string; errors?: any }); - // (undocumented) - errors?: any; -} - -// @public -export type CheckValidationResponse = { - valid: boolean; - message?: string; - errors?: any; -}; - -// @public -export interface FactChecker< - CheckType extends TechInsightCheck, - CheckResultType extends CheckResult, -> { - addCheck(check: CheckType): Promise; - getChecks(): Promise; - runChecks(entity: string, checks: string[]): Promise; - validate(check: CheckType): Promise; -} - -// @public -export interface FactCheckerFactory< - CheckType extends TechInsightCheck, - CheckResultType extends CheckResult, -> { - // (undocumented) - construct( - repository: TechInsightsStore, - ): FactChecker; -} - // @public export type FactResponse = { [id: string]: { @@ -78,113 +39,5 @@ export type FactResponse = { }; }; -// @public -export interface FactRetriever { - entityTypes?: string[]; - handler: (ctx: FactRetrieverContext) => Promise; - id: string; - schema: FactSchema; - version: string; -} - -// @public -export type FactRetrieverContext = { - config: Config; - discovery: PluginEndpointDiscovery; - logger: Logger_2; -}; - -// @public -export type FactRetrieverRegistration = { - factRetriever: FactRetriever; - cadence?: string; -}; - -// @public -export type FactSchema = { - [name: string]: { - type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; - description: string; - since?: string; - metadata?: Record; - }; -}; - -// Warning: (ae-missing-release-tag) "FactSchemaDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type FactSchemaDefinition = Omit; - -// @public -export type FlatTechInsightFact = TechInsightFact & { - id: string; -}; - -// @public -export interface TechInsightCheck { - description: string; - factIds: string[]; - failureMetadata?: Record; - id: string; - name: string; - successMetadata?: Record; - type: string; -} - -// @public -export interface TechInsightCheckRegistry { - // (undocumented) - get(checkId: string): Promise; - // (undocumented) - getAll(checks: string[]): Promise; - // (undocumented) - list(): Promise; - // (undocumented) - register(check: CheckType): Promise; -} - -// @public -export type TechInsightFact = { - entity: { - namespace: string; - kind: string; - name: string; - }; - facts: Record< - string, - | number - | string - | boolean - | DateTime - | number[] - | string[] - | boolean[] - | DateTime[] - >; - timestamp?: DateTime; -}; - -// @public -export interface TechInsightsStore { - getFactsBetweenTimestampsByIds( - ids: string[], - entity: string, - startDateTime: DateTime, - endDateTime: DateTime, - ): Promise<{ - [factRef: string]: FlatTechInsightFact[]; - }>; - // (undocumented) - getLatestFactsByIds( - ids: string[], - entity: string, - ): Promise<{ - [factRef: string]: FlatTechInsightFact; - }>; - getLatestSchemas(ids?: string[]): Promise; - insertFacts(id: string, facts: TechInsightFact[]): Promise; - insertFactSchema(schemaDefinition: FactSchemaDefinition): Promise; -} - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 37d66a9379..4c287c26f3 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -30,11 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/config": "^0.1.8", "@types/luxon": "^2.0.5", - "luxon": "^2.0.2", - "winston": "^3.2.1" + "luxon": "^2.0.2" }, "devDependencies": { "@backstage/cli": "^0.7.1" diff --git a/plugins/tech-insights-common/src/index.ts b/plugins/tech-insights-common/src/index.ts index 982ae0ac41..4bfc660748 100644 --- a/plugins/tech-insights-common/src/index.ts +++ b/plugins/tech-insights-common/src/index.ts @@ -14,7 +14,111 @@ * limitations under the License. */ -export * from './checks'; -export * from './facts'; -export * from './persistence'; -export * from './responses'; +import { DateTime } from 'luxon'; + +/** + * @public + * + * Response type for checks. + */ +export interface CheckResponse { + /** + * Identifier of the Check + */ + id: string; + /** + * Type identifier for the check. + * Can be used to determine storage options, logical routing to correct FactChecker implementation + * or to help frontend render correct component types based on this + */ + type: string; + /** + * Human readable name of the Check + */ + name: string; + /** + * Description of the Check + */ + description: string; + + /** + * A collection of references to fact rows used to run this checks against + */ + factIds: string[]; + + /** + * Metadata related to a check. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + */ + metadata?: Record; +} + +/** + * @public + * + * Individual fact response type. + * Keyed by the name of the fact + */ +export type FactResponse = { + [id: string]: { + /** + * Reference and unique identifier of the fact row + */ + id: string; + /** + * Type of the individual fact value + * + * Numbers are split into integers and floating point values. + * `set` indicates a collection of values + */ + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + + /** + * Description of the individual fact + */ + description: string; + + /** + * Actual value of the fact + */ + value: number | string | boolean | DateTime | []; + + /** + * An optional SemVer version identifying when this fact was added to the FactSchema + */ + since?: string; + + /** + * Metadata related to an individual fact. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + */ + metadata?: Record; + }; +}; + +/** + * Generic CheckResult + * + * Contains information about the facts used to calculate the check result + * and information about the check itself. Both may include metadata to be able to display additional information. + * A collection of these should be parseable by the frontend to display scorecards + * + * @public + */ +export type CheckResult = { + facts: FactResponse; + check: CheckResponse; +}; + +/** + * CheckResult of type Boolean. + * + * @public + */ +export interface BooleanCheckResult extends CheckResult { + result: boolean; +} diff --git a/plugins/tech-insights-common/src/responses.ts b/plugins/tech-insights-common/src/responses.ts deleted file mode 100644 index c3dee1df48..0000000000 --- a/plugins/tech-insights-common/src/responses.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 { DateTime } from 'luxon'; - -/** - * @public - * - * Response type for checks. - */ -export interface CheckResponse { - /** - * Identifier of the Check - */ - id: string; - /** - * Type identifier for the check. - * Can be used to determine storage options, logical routing to correct FactChecker implementation - * or to help frontend render correct component types based on this - */ - type: string; - /** - * Human readable name of the Check - */ - name: string; - /** - * Description of the Check - */ - description: string; - - /** - * A collection of references to fact rows used to run this checks against - */ - factIds: string[]; - - /** - * Metadata related to a check. - * Can contain links, additional description texts and other actionable data. - * - * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined - */ - metadata?: Record; -} - -/** - * @public - * - * Individual fact response type. - * Keyed by the name of the fact - */ -export type FactResponse = { - [id: string]: { - /** - * Reference and unique identifier of the fact row - */ - id: string; - /** - * Type of the individual fact value - * - * Numbers are split into integers and floating point values. - * `set` indicates a collection of values - */ - type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; - - /** - * Description of the individual fact - */ - description: string; - - /** - * Actual value of the fact - */ - value: number | string | boolean | DateTime | []; - - /** - * An optional SemVer version identifying when this fact was added to the FactSchema - */ - since?: string; - - /** - * Metadata related to an individual fact. - * Can contain links, additional description texts and other actionable data. - * - * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined - */ - metadata?: Record; - }; -}; diff --git a/plugins/tech-insights-node/.eslintrc.js b/plugins/tech-insights-node/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/tech-insights-node/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/tech-insights-node/README.md b/plugins/tech-insights-node/README.md new file mode 100644 index 0000000000..4065611837 --- /dev/null +++ b/plugins/tech-insights-node/README.md @@ -0,0 +1,3 @@ +# Tech Insights Node + +Common types and functionalities for tech insights backend implementations to be shared between tech-insights-backend and individual implementations of FactRetrievers, FactCheckers and their persistence options. diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json new file mode 100644 index 0000000000..e5620322fd --- /dev/null +++ b/plugins/tech-insights-node/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-tech-insights-node", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "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": "plugins/tech-insights-node" + }, + "keywords": [ + "backstage", + "tech-insights" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "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.0", + "@backstage/config": "^0.1.8", + "@backstage/plugin-tech-insights-common": "^0.1.0 ", + "@types/luxon": "^2.0.5", + "luxon": "^2.0.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.7.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights-common/src/checks.ts b/plugins/tech-insights-node/src/checks.ts similarity index 77% rename from plugins/tech-insights-common/src/checks.ts rename to plugins/tech-insights-node/src/checks.ts index 90fa7ee34f..e5f10aff6d 100644 --- a/plugins/tech-insights-common/src/checks.ts +++ b/plugins/tech-insights-node/src/checks.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { TechInsightsStore } from './persistence'; -import { CheckResponse, FactResponse } from './responses'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; /** * A factory wrapper to construct FactChecker implementations. @@ -57,16 +57,7 @@ export interface FactChecker< * @param checks - A collection of checks to run against provided entity * @returns - A collection containing check/fact information and the actual results of the check */ - runChecks(entity: string, checks: string[]): Promise; - - /** - * Adds and stores new checks so they can be run checks against. - * Implementation should ideally run validation against the check. - * - * @param check - The actual check to be added. - * @returns - An indicator if fact was successfully added - */ - addCheck(check: CheckType): Promise; + runChecks(entity: string, checks?: string[]): Promise; /** * Retrieves all available checks that can be used to run checks against. @@ -99,29 +90,6 @@ export interface TechInsightCheckRegistry { list(): Promise; } -/** - * Generic CheckResult - * - * Contains information about the facts used to calculate the check result - * and information about the check itself. Both may include metadata to be able to display additional information. - * A collection of these should be parseable by the frontend to display scorecards - * - * @public - */ -export type CheckResult = { - facts: FactResponse; - check: CheckResponse; -}; - -/** - * CheckResult of type Boolean. - * - * @public - */ -export interface BooleanCheckResult extends CheckResult { - result: boolean; -} - /** * Generic definition of a check for Tech Insights * @@ -181,21 +149,5 @@ export interface TechInsightCheck { export type CheckValidationResponse = { valid: boolean; message?: string; - errors?: any; + errors?: unknown[]; }; - -/** - * Error object for Validation Errors - * - * Can be used to short circuit larger execution paths instead of passing validation response around - * - * @public - */ -export class CheckValidationError extends Error { - errors?: any; - - constructor({ message, errors }: { message: string; errors?: any }) { - super(message); - this.errors = errors; - } -} diff --git a/plugins/tech-insights-common/src/facts.ts b/plugins/tech-insights-node/src/facts.ts similarity index 92% rename from plugins/tech-insights-common/src/facts.ts rename to plugins/tech-insights-node/src/facts.ts index a46e7bd95d..b912895ec6 100644 --- a/plugins/tech-insights-common/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -175,14 +175,16 @@ export interface FactRetriever { schema: FactSchema; /** - * An optional list of entity type descriptors to indicate if this fact retriever is valid for an entity type. + * An optional object/array of objects of entity filters to indicate if this fact retriever is valid for an entity type. * If omitted, the retriever should apply to all entities. * - * Should be defined as: - * ['component', 'group', 'user'] for top level items - * ['component:service', 'component:website'] for component types. + * Should be defined for example: + * { field: 'kind', values: ['component'] } + * { field: 'metadata.name', values: ['component-1', 'component-2'] } */ - entityTypes?: string[]; + entityFilter?: + | Record[] + | Record; } export type FactSchemaDefinition = Omit; diff --git a/plugins/tech-insights-node/src/index.test.ts b/plugins/tech-insights-node/src/index.test.ts new file mode 100644 index 0000000000..5e5f8c9d77 --- /dev/null +++ b/plugins/tech-insights-node/src/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as anything from './'; + +describe('tech-insights-node', () => { + // Types only + it('should exist', () => { + expect(anything).toBeTruthy(); + }); +}); diff --git a/plugins/tech-insights-node/src/index.ts b/plugins/tech-insights-node/src/index.ts new file mode 100644 index 0000000000..70ef27e159 --- /dev/null +++ b/plugins/tech-insights-node/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './checks'; +export * from './facts'; +export * from './persistence'; diff --git a/plugins/tech-insights-common/src/persistence.ts b/plugins/tech-insights-node/src/persistence.ts similarity index 100% rename from plugins/tech-insights-common/src/persistence.ts rename to plugins/tech-insights-node/src/persistence.ts diff --git a/plugins/tech-insights-common/src/setupTests.ts b/plugins/tech-insights-node/src/setupTests.ts similarity index 100% rename from plugins/tech-insights-common/src/setupTests.ts rename to plugins/tech-insights-node/src/setupTests.ts From da4577d73183d641607e87a7102335aea8d6f680 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Thu, 28 Oct 2021 16:25:53 +0200 Subject: [PATCH 216/245] Un-yikesify example FactRetriever cron to not pollute the DB Signed-off-by: Jussi Hallila --- packages/backend/src/plugins/techInsights.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index 9927d98f87..6e10b60370 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -38,7 +38,7 @@ export default async function createPlugin({ database, discovery, factRetrievers: [ - createFactRetrieverRegistration('* * * * *', { + createFactRetrieverRegistration('5 4 * * 6', { id: 'testRetriever', version: '1.1.2', entityFilter: [{ kind: 'component' }], // EntityFilter to be used in the future (creating checks, graphs etc.) to figure out which entities this fact retrieves data for. From 29faa2ace2149abbc782ce5f3c2f80db2eb87d99 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Fri, 29 Oct 2021 10:09:34 +0200 Subject: [PATCH 217/245] Update cli version Signed-off-by: Jussi Hallila --- plugins/tech-insights-backend-module-jsonfc/package.json | 2 +- plugins/tech-insights-backend/package.json | 2 +- plugins/tech-insights-common/package.json | 2 +- plugins/tech-insights-node/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index a1db34a97e..8943b0e461 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -43,7 +43,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.8.0", "@types/node-cron": "^2.0.4" }, "files": [ diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index cf1e1d1c6f..cd437ee16b 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -52,7 +52,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 4c287c26f3..5e02c9e4b0 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -34,7 +34,7 @@ "luxon": "^2.0.2" }, "devDependencies": { - "@backstage/cli": "^0.7.1" + "@backstage/cli": "^0.8.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index e5620322fd..4a7cf3589b 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -38,7 +38,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.1" + "@backstage/cli": "^0.8.0" }, "files": [ "dist" From 5ebfedd9c2bf8f1acc6f3613b27abef9a331c8fd Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Fri, 29 Oct 2021 12:29:27 +0200 Subject: [PATCH 218/245] Add new api-report Update persistence context to not be wrapped into a useless class. Modify the way test databases are created. Signed-off-by: Jussi Hallila --- packages/backend/src/plugins/techInsights.ts | 5 +- .../README.md | 30 ++-- plugins/tech-insights-backend/README.md | 52 +++--- plugins/tech-insights-backend/package.json | 1 + plugins/tech-insights-backend/src/index.ts | 2 +- .../service/persistence/DatabaseManager.ts | 99 ----------- .../persistence/TechInsightsDatabase.test.ts | 15 +- .../service/persistence/persistenceContext.ts | 59 +++++++ .../src/service/router.test.ts | 2 +- .../src/service/router.ts | 2 +- .../src/service/techInsightsContextBuilder.ts | 6 +- plugins/tech-insights-node/api-report.md | 155 ++++++++++++++++++ 12 files changed, 278 insertions(+), 150 deletions(-) delete mode 100644 plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts create mode 100644 plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts create mode 100644 plugins/tech-insights-node/api-report.md diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index 6e10b60370..abab1d133d 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -39,6 +39,7 @@ export default async function createPlugin({ discovery, factRetrievers: [ createFactRetrieverRegistration('5 4 * * 6', { + // Example cron, At 04:05 on Saturday. id: 'testRetriever', version: '1.1.2', entityFilter: [{ kind: 'component' }], // EntityFilter to be used in the future (creating checks, graphs etc.) to figure out which entities this fact retrieves data for. @@ -52,7 +53,9 @@ export default async function createPlugin({ const catalogClient = new CatalogClient({ discoveryApi: discovery, }); - const entities = await catalogClient.getEntities(); + const entities = await catalogClient.getEntities({ + filter: [{ kind: 'component' }], + }); return Promise.resolve( entities.items.map(it => { diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index 1404bd4ea4..a19c3dd2ce 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -24,31 +24,31 @@ and modify the `techInsights.ts` file to contain a reference to the FactCheckers + logger, + }), -const builder = new DefaultTechInsightsBuilder({ -logger, -config, -database, -discovery, -factRetrievers: [myFactRetrieverRegistration], -+ factCheckerFactory: myFactCheckerFactory -}); + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory + }); ``` By default this implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows ```diff -const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip -const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ - checks: [], - logger, -+ checkRegistry: myTechInsightCheckRegistry -}), + const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip + const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, ++ checkRegistry: myTechInsightCheckRegistry + }), ``` ## Adding checks -Checks for this FactChecker are constructed as `json-rules-engine` compatible JSON rules. A check could look like the following for example: +Checks for this FactChecker are constructed as [`json-rules-engine` compatible JSON rules](https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#conditions). A check could look like the following for example: ```ts import { TechInsightJsonRuleCheck } from '../types'; diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 2de16b65b0..e7a78d34a2 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -53,20 +53,20 @@ With the `techInsights.ts` router setup in place, add the router to `packages/backend/src/index.ts`: ```diff -+import techInsights from './plugins/techInsights'; ++ import techInsights from './plugins/techInsights'; -async function main() { - ... - const createEnv = makeCreateEnv(config); + async function main() { + ... + const createEnv = makeCreateEnv(config); - const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); -+ const techInsightsEnv = useHotMemoize(module, () => createEnv('tech_insights')); - - const apiRouter = Router(); -+ apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); - ... - apiRouter.use(notFoundHandler()); + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const techInsightsEnv = useHotMemoize(module, () => createEnv('tech_insights')); + const apiRouter = Router(); ++ apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); + ... + apiRouter.use(notFoundHandler()); + } ``` ### Adding fact retrievers @@ -95,10 +95,10 @@ Then you can modify the example `techInsights.ts` file shown above like this: ```diff const builder = new DefaultTechInsightsBuilder({ -logger, -config, -database, -discovery, + logger, + config, + database, + discovery, - factRetrievers: [], + factRetrievers: [myFactRetrieverRegistration], }); @@ -119,12 +119,12 @@ An example implementation of a FactRetriever could for example be as follows: const myFactRetriever: FactRetriever = { id: 'documentation-number-factretriever', // unique identifier of the fact retriever version: '0.1.1', // SemVer version number of this fact retriever schema. This should be incremented if the implementation changes + entityFilter: [{ kind: 'component' }], // EntityFilter to be used in the future (creating checks, graphs etc.) to figure out which entities this fact retrieves data for. schema: { // Name/identifier of an individual fact that this retriever returns examplenumberfact: { type: 'integer', // Type of the fact description: 'A fact of a number', // Description of the fact - entityTypes: ['component'], // An array of entity kinds that this fact is applicable to }, }, handler: async ctx => { @@ -133,7 +133,9 @@ const myFactRetriever: FactRetriever = { const catalogClient = new CatalogClient({ discoveryApi: discovery, }); - const entities = await catalogClient.getEntities(); // Retrieve all entities + const entities = await catalogClient.getEntities({ + filter: [{ kind: 'component' }], + }); /** * snip: Do complex logic to retrieve facts from external system or calculate fact values */ @@ -183,14 +185,14 @@ and modify the `techInsights.ts` file to contain a reference to the FactChecker + logger, + }), -const builder = new DefaultTechInsightsBuilder({ -logger, -config, -database, -discovery, -factRetrievers: [myFactRetrieverRegistration], -+ factCheckerFactory: myFactCheckerFactory -}); + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory + }); ``` To be able to run checks, you need to additionally add individual checks into your FactChecker implementation. For examples how to add these, you can check the documentation of the individual implementation of the FactChecker diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index cd437ee16b..678bec25c4 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -52,6 +52,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.8", "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", diff --git a/plugins/tech-insights-backend/src/index.ts b/plugins/tech-insights-backend/src/index.ts index 2a74bc6b88..5e8e690e8d 100644 --- a/plugins/tech-insights-backend/src/index.ts +++ b/plugins/tech-insights-backend/src/index.ts @@ -23,5 +23,5 @@ export type { TechInsightsContext, } from './service/techInsightsContextBuilder'; -export type { PersistenceContext } from './service/persistence/DatabaseManager'; +export type { PersistenceContext } from './service/persistence/persistenceContext'; export { createFactRetrieverRegistration } from './service/fact/createFactRetriever'; diff --git a/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts b/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts deleted file mode 100644 index 126c253fa5..0000000000 --- a/plugins/tech-insights-backend/src/service/persistence/DatabaseManager.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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, resolvePackagePath } from '@backstage/backend-common'; -import knexFactory, { Knex } from 'knex'; -import { Logger } from 'winston'; -import { v4 as uuidv4 } from 'uuid'; -import { TechInsightsDatabase } from './TechInsightsDatabase'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; - -const migrationsDir = resolvePackagePath( - '@backstage/plugin-tech-insights-backend', - 'migrations', -); - -/** - * A Container for persistence related components in TechInsights - * - * @public - */ -export type PersistenceContext = { - techInsightsStore: TechInsightsStore; -}; - -export type CreateDatabaseOptions = { - logger: Logger; -}; - -const defaultOptions: CreateDatabaseOptions = { - logger: getVoidLogger(), -}; - -/** - * A factory class to construct persistence context for both running implmentation and test cases. - * - * @public - */ -export class DatabaseManager { - public static async initializePersistenceContext( - knex: Knex, - options: CreateDatabaseOptions = defaultOptions, - ): Promise { - await knex.migrate.latest({ - directory: migrationsDir, - }); - return { - techInsightsStore: new TechInsightsDatabase(knex, options.logger), - }; - } - - public static async createTestDatabase( - knex: Knex, - ): Promise { - const knexInstance = knex ?? (await this.createTestDatabaseConnection()); - return await this.initializePersistenceContext(knexInstance); - } - - public static async createTestDatabaseConnection(): Promise { - const config: Knex.Config = { - client: 'sqlite3', - connection: ':memory:', - useNullAsDefault: true, - }; - - let knexInstance = knexFactory(config); - if (typeof config.connection !== 'string') { - const tempDbName = `d${uuidv4().replace(/-/g, '')}`; - await knexInstance.raw(`CREATE DATABASE ${tempDbName};`); - knexInstance = knexFactory({ - ...config, - connection: { - ...config.connection, - database: tempDbName, - }, - }); - } - - knexInstance.client.pool.on( - 'createSuccess', - (_eventId: any, resource: any) => { - resource.run('PRAGMA foreign_keys = ON', () => {}); - }, - ); - - return knexInstance; - } -} diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts index 85d6098ce3..43d00e2425 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DatabaseManager } from './DatabaseManager'; import { DateTime, Duration } from 'luxon'; import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { Knex } from 'knex'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { getVoidLogger } from '@backstage/backend-common'; +import { initializePersistenceContext } from './persistenceContext'; const factSchemas = [ { @@ -117,9 +119,14 @@ describe('Tech Insights database', () => { let store: TechInsightsStore; let testDbClient: Knex; beforeAll(async () => { - testDbClient = await DatabaseManager.createTestDatabaseConnection(); - store = (await DatabaseManager.createTestDatabase(testDbClient)) - .techInsightsStore; + testDbClient = await TestDatabases.create().init('SQLITE_3'); + + store = ( + await initializePersistenceContext(testDbClient, { + logger: getVoidLogger(), + }) + ).techInsightsStore; + await testDbClient.batchInsert('fact_schemas', factSchemas); await testDbClient.batchInsert('facts', facts); }); diff --git a/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts b/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts new file mode 100644 index 0000000000..b0fb65349f --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts @@ -0,0 +1,59 @@ +/* + * 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, resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { Logger } from 'winston'; +import { TechInsightsDatabase } from './TechInsightsDatabase'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-tech-insights-backend', + 'migrations', +); + +/** + * A Container for persistence related components in TechInsights + * + * @public + */ +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + +export type CreateDatabaseOptions = { + logger: Logger; +}; + +const defaultOptions: CreateDatabaseOptions = { + logger: getVoidLogger(), +}; + +/** + * A factory method to construct persistence context for running implementation. + * + * @public + */ +export const initializePersistenceContext = async ( + knex: Knex, + options: CreateDatabaseOptions = defaultOptions, +): Promise => { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return { + techInsightsStore: new TechInsightsDatabase(knex, options.logger), + }; +}; diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index 8a7b6bdbb3..0b7d3b7c45 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -19,7 +19,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import request from 'supertest'; import express from 'express'; -import { PersistenceContext } from './persistence/DatabaseManager'; +import { PersistenceContext } from './persistence/persistenceContext'; import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { DateTime } from 'luxon'; import { Knex } from 'knex'; diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index 752534928f..a8951318ea 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -25,7 +25,7 @@ import { import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Logger } from 'winston'; import { DateTime } from 'luxon'; -import { PersistenceContext } from './persistence/DatabaseManager'; +import { PersistenceContext } from './persistence/persistenceContext'; import { EntityRef, parseEntityName, diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index 7305f1778c..75e24b7c06 100644 --- a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -29,9 +29,9 @@ import { TechInsightCheck, } from '@backstage/plugin-tech-insights-node'; import { - DatabaseManager, + initializePersistenceContext, PersistenceContext, -} from './persistence/DatabaseManager'; +} from './persistence/persistenceContext'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; /** @@ -105,7 +105,7 @@ export const buildTechInsightsContext = async < const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); - const persistenceContext = await DatabaseManager.initializePersistenceContext( + const persistenceContext = await initializePersistenceContext( await database.getClient(), { logger }, ); diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md new file mode 100644 index 0000000000..7a157309dd --- /dev/null +++ b/plugins/tech-insights-node/api-report.md @@ -0,0 +1,155 @@ +## API Report File for "@backstage/plugin-tech-insights-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Config } from '@backstage/config'; +import { DateTime } from 'luxon'; +import { Logger as Logger_2 } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public +export type CheckValidationResponse = { + valid: boolean; + message?: string; + errors?: unknown[]; +}; + +// @public +export interface FactChecker< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + getChecks(): Promise; + runChecks(entity: string, checks?: string[]): Promise; + validate(check: CheckType): Promise; +} + +// @public +export interface FactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + // (undocumented) + construct( + repository: TechInsightsStore, + ): FactChecker; +} + +// @public +export interface FactRetriever { + // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag + // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" + // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag + // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" + entityFilter?: + | Record[] + | Record; + handler: (ctx: FactRetrieverContext) => Promise; + id: string; + schema: FactSchema; + version: string; +} + +// @public +export type FactRetrieverContext = { + config: Config; + discovery: PluginEndpointDiscovery; + logger: Logger_2; +}; + +// @public +export type FactRetrieverRegistration = { + factRetriever: FactRetriever; + cadence?: string; +}; + +// @public +export type FactSchema = { + [name: string]: { + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + description: string; + since?: string; + metadata?: Record; + }; +}; + +// Warning: (ae-missing-release-tag) "FactSchemaDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type FactSchemaDefinition = Omit; + +// @public +export type FlatTechInsightFact = TechInsightFact & { + id: string; +}; + +// @public +export interface TechInsightCheck { + description: string; + factIds: string[]; + failureMetadata?: Record; + id: string; + name: string; + successMetadata?: Record; + type: string; +} + +// @public +export interface TechInsightCheckRegistry { + // (undocumented) + get(checkId: string): Promise; + // (undocumented) + getAll(checks: string[]): Promise; + // (undocumented) + list(): Promise; + // (undocumented) + register(check: CheckType): Promise; +} + +// @public +export type TechInsightFact = { + entity: { + namespace: string; + kind: string; + name: string; + }; + facts: Record< + string, + | number + | string + | boolean + | DateTime + | number[] + | string[] + | boolean[] + | DateTime[] + >; + timestamp?: DateTime; +}; + +// @public +export interface TechInsightsStore { + getFactsBetweenTimestampsByIds( + ids: string[], + entity: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ + [factRef: string]: FlatTechInsightFact[]; + }>; + // (undocumented) + getLatestFactsByIds( + ids: string[], + entity: string, + ): Promise<{ + [factRef: string]: FlatTechInsightFact; + }>; + getLatestSchemas(ids?: string[]): Promise; + insertFacts(id: string, facts: TechInsightFact[]): Promise; + insertFactSchema(schemaDefinition: FactSchemaDefinition): Promise; +} + +// (No @packageDocumentation comment for this package) +``` From 9c19d6ad0179834ebc3a9b34f1eac5f792dd36e5 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Fri, 29 Oct 2021 12:42:31 +0200 Subject: [PATCH 219/245] Tweak formatting of snippets on README.md files. Signed-off-by: Jussi Hallila --- .../README.md | 38 +++++------ plugins/tech-insights-backend/README.md | 68 ++++++++++++------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index a19c3dd2ce..48c1e69919 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -17,32 +17,32 @@ yarn add @backstage/plugin-tech-insights-backend-module-jsonfc and modify the `techInsights.ts` file to contain a reference to the FactCheckers implementation. ```diff -+ import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; ++import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; -+ const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ -+ checks: [], -+ logger, -+ }), ++const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ ++ checks: [], ++ logger, ++}), - const builder = new DefaultTechInsightsBuilder({ - logger, - config, - database, - discovery, - factRetrievers: [myFactRetrieverRegistration], -+ factCheckerFactory: myFactCheckerFactory - }); + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory + }); ``` By default this implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows ```diff - const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip - const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ - checks: [], - logger, -+ checkRegistry: myTechInsightCheckRegistry - }), + const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip + const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, ++ checkRegistry: myTechInsightCheckRegistry + }), ``` diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index e7a78d34a2..e2cc1c0252 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -53,20 +53,20 @@ With the `techInsights.ts` router setup in place, add the router to `packages/backend/src/index.ts`: ```diff -+ import techInsights from './plugins/techInsights'; ++import techInsights from './plugins/techInsights'; - async function main() { - ... - const createEnv = makeCreateEnv(config); + async function main() { + ... + const createEnv = makeCreateEnv(config); - const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); -+ const techInsightsEnv = useHotMemoize(module, () => createEnv('tech_insights')); + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const techInsightsEnv = useHotMemoize(module, () => createEnv('tech_insights')); - const apiRouter = Router(); -+ apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); - ... - apiRouter.use(notFoundHandler()); - } + const apiRouter = Router(); ++ apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); + ... + apiRouter.use(notFoundHandler()); + } ``` ### Adding fact retrievers @@ -104,14 +104,32 @@ const builder = new DefaultTechInsightsBuilder({ }); ``` +#### Running fact retrievers in a multi-instance installation + +Current logic on running scheduled fact retrievers is intended to be executed in a single instance. Running on multi-instane environment there might be some additional data accumulation when multiple fact retrievers would retrieve and persist their facts. To mitigate this it is recommended to mark a single instance to be a specific fact retriever instance. One way to do this is by using environment variables to indicate if the retrievers should be registered. This can be done for example like the code snippet below + +```diff +const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, +- factRetrievers: [], ++ factRetrievers: process.env.MAIN_FACT_RETRIEVER_INSTANCE ? [myFactRetrieverRegistration] : [], +}); +``` + +Where the instance dedicated to handling retrieval of facts would have environment variable `MAIN_FACT_RETRIEVER_INSTANCE` set to true. + ### Creating Fact Retrievers -A Fact Retriever consist of four parts: +A Fact Retriever consist of four required and one optional parts: 1. `id` - unique identifier of a fact retriever 2. `version`: A semver string indicating the current version of the schema and the handler 3. `schema` - A versioned schema defining the shape of data a fact retriever returns 4. `handler` - An asynchronous function handling the logic of retrieving and returning facts for an entity +5. `entityFilter` - (Optional) EntityFilter object defining the entity kinds, types and/or names this fact retriever handles An example implementation of a FactRetriever could for example be as follows: @@ -178,21 +196,21 @@ yarn add @backstage/plugin-tech-insights-backend-module-jsonfc and modify the `techInsights.ts` file to contain a reference to the FactChecker implementation. ```diff -+ import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; ++import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; -+ const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ -+ checks: [], -+ logger, -+ }), ++const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ ++ checks: [], ++ logger, ++}), - const builder = new DefaultTechInsightsBuilder({ - logger, - config, - database, - discovery, - factRetrievers: [myFactRetrieverRegistration], -+ factCheckerFactory: myFactCheckerFactory - }); + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory + }); ``` To be able to run checks, you need to additionally add individual checks into your FactChecker implementation. For examples how to add these, you can check the documentation of the individual implementation of the FactChecker From b863feefbb48466b39cfecb023ef5112df60d753 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Fri, 29 Oct 2021 15:14:51 +0200 Subject: [PATCH 220/245] Address warnings within api-docs Move extra info on doc to be under remarks block Signed-off-by: Jussi Hallila --- .../src/service/fact/createFactRetriever.ts | 9 ++++++--- plugins/tech-insights-node/api-report.md | 8 +------- plugins/tech-insights-node/src/facts.ts | 10 ++++++++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts index b744d3b826..655736400a 100644 --- a/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts @@ -23,6 +23,12 @@ import { * * A helper function to construct fact retriever registrations. * + * @param cadence - cron expression to indicate when the fact retriever should be triggered + * @param factRetriever - Implementation of fact retriever consisting of at least id, version, schema and handler + * + * + * @remarks + * * Cron expressions help: * ┌────────────── second (optional) # │ ┌──────────── minute @@ -34,9 +40,6 @@ import { # │ │ │ │ │ │ # * * * * * * * - * - * @param cadence - cron expression to indicate when the fact retriever should be triggered - * @param factRetriever - Implementation of fact retriever consisting of at least id, version, schema and handler */ export function createFactRetrieverRegistration( cadence: string, diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 7a157309dd..caf4b4cf0e 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -39,10 +39,6 @@ export interface FactCheckerFactory< // @public export interface FactRetriever { - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" entityFilter?: | Record[] | Record; @@ -75,9 +71,7 @@ export type FactSchema = { }; }; -// Warning: (ae-missing-release-tag) "FactSchemaDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type FactSchemaDefinition = Omit; // @public diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index b912895ec6..a52a91f2f4 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -179,14 +179,20 @@ export interface FactRetriever { * If omitted, the retriever should apply to all entities. * * Should be defined for example: - * { field: 'kind', values: ['component'] } - * { field: 'metadata.name', values: ['component-1', 'component-2'] } + * \{ field: 'kind', values: \['component'\] \} + * \{ field: 'metadata.name', values: \['component-1', 'component-2'\] \} */ entityFilter?: | Record[] | Record; } +/** + * @public + * + * A flat serializable structure for Facts. + * Containing information about fact schema, version, id, and entity filters + */ export type FactSchemaDefinition = Omit; /** From 1bbaa719ff2d254571df03d870e1319029d46176 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:10:13 +0200 Subject: [PATCH 221/245] user-settings: remove duplicate core-plugin-api dependency Signed-off-by: Patrik Oldsberg --- plugins/user-settings/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 9a3b65f7ae..9ec642892b 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -45,7 +45,6 @@ "devDependencies": { "@backstage/cli": "^0.8.1", "@backstage/core-app-api": "^0.1.19", - "@backstage/core-plugin-api": "^0.1.11", "@backstage/dev-utils": "^0.2.12", "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", From dd355bca467bbcfa88fe7fec69a3f17abd70626f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:14:25 +0200 Subject: [PATCH 222/245] cli: dynamically determine which packages are unsafe to repack Signed-off-by: Patrik Oldsberg --- .changeset/angry-countries-cheat.md | 5 +++++ packages/cli/src/lib/packager/index.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/angry-countries-cheat.md diff --git a/.changeset/angry-countries-cheat.md b/.changeset/angry-countries-cheat.md new file mode 100644 index 0000000000..e97e0ae56c --- /dev/null +++ b/.changeset/angry-countries-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Switched to dynamically determining the packages that are unsafe to repack when executing the CLI within the Backstage main repo. diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 6917795e83..dba42e6013 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -24,14 +24,16 @@ import { tmpdir } from 'os'; import tar, { CreateOptions } from 'tar'; import { paths } from '../paths'; import { run } from '../run'; -import { packageVersions } from '../version'; import { ParallelOption } from '../parallel'; +import { + dependencies as cliDependencies, + devDependencies as cliDevDependencies, +} from '../../../package.json'; // These packages aren't safe to pack in parallel since the CLI depends on them const UNSAFE_PACKAGES = [ - ...Object.keys(packageVersions), - '@backstage/cli-common', - '@backstage/config-loader', + ...Object.keys(cliDependencies), + ...Object.keys(cliDevDependencies), ]; type LernaPackage = { From f1903c71676b0c36ef448958d78720c18d2716b2 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 29 Oct 2021 16:47:02 +0200 Subject: [PATCH 223/245] add warning to dismissableBanner Signed-off-by: Samira Mokaram --- .../DismissableBanner/DismissableBanner.stories.tsx | 11 +++++++++++ .../DismissableBanner/DismissableBanner.tsx | 5 ++++- packages/theme/src/themes.ts | 1 + packages/theme/src/types.ts | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx index a64514be61..e0e9a89c9f 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx @@ -105,3 +105,14 @@ export const Fixed = () => (
); +export const Warning = () => ( +
+ + + +
+); diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 1213111c9d..40082925c6 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -79,12 +79,15 @@ const useStyles = makeStyles( error: { backgroundColor: theme.palette.banner.error, }, + warning: { + backgroundColor: theme.palette.banner.warning, + }, }), { name: 'BackstageDismissableBanner' }, ); type Props = { - variant: 'info' | 'error'; + variant: 'info' | 'error' | 'warning'; message: ReactNode; id: string; fixed?: boolean; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 3eb1bcc252..f35c45ef7d 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -55,6 +55,7 @@ export const lightTheme = createTheme({ error: '#E22134', text: '#FFFFFF', link: '#000000', + warning: '#FF9800', }, border: '#E6E6E6', textContrast: '#000000', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index db3e89fa4b..40f45ee739 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -76,6 +76,7 @@ export type BackstagePaletteAdditions = { error: string; text: string; link: string; + warning: string; }; }; From c11a37710afb2d653d44a82ae9d6072923beabb7 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 29 Oct 2021 16:51:32 +0200 Subject: [PATCH 224/245] add changeset Signed-off-by: Samira Mokaram --- .changeset/dry-spies-cover.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/dry-spies-cover.md diff --git a/.changeset/dry-spies-cover.md b/.changeset/dry-spies-cover.md new file mode 100644 index 0000000000..3911fdde84 --- /dev/null +++ b/.changeset/dry-spies-cover.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/theme': patch +--- + +Will Add warning variant to Dismissable component. From 83cc72f6cbddce7a46b35353c9545081e2498d42 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 29 Oct 2021 17:02:48 +0200 Subject: [PATCH 225/245] fix Signed-off-by: Samira Mokaram --- .changeset/dry-spies-cover.md | 2 +- packages/theme/src/themes.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/dry-spies-cover.md b/.changeset/dry-spies-cover.md index 3911fdde84..91c6249d13 100644 --- a/.changeset/dry-spies-cover.md +++ b/.changeset/dry-spies-cover.md @@ -3,4 +3,4 @@ '@backstage/theme': patch --- -Will Add warning variant to Dismissable component. +Will Add warning variant to DismissableBanner component. diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index f35c45ef7d..7e99ce8e8a 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -130,6 +130,7 @@ export const darkTheme = createTheme({ error: '#E22134', text: '#FFFFFF', link: '#000000', + warning: '#FF9800', }, border: '#E6E6E6', textContrast: '#FFFFFF', From c493f261e2b48c3cbbacd4ecc1303859b919d4be Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 29 Oct 2021 17:32:36 +0200 Subject: [PATCH 226/245] fix changeset Signed-off-by: Samira Mokaram --- .changeset/dry-spies-cover.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dry-spies-cover.md b/.changeset/dry-spies-cover.md index 91c6249d13..7006dee5c4 100644 --- a/.changeset/dry-spies-cover.md +++ b/.changeset/dry-spies-cover.md @@ -3,4 +3,4 @@ '@backstage/theme': patch --- -Will Add warning variant to DismissableBanner component. +Will Add warning variant to `DismissableBanner` component. From a300f19a933b83a0a30e6b8279786eba94499719 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 29 Oct 2021 17:45:54 +0200 Subject: [PATCH 227/245] add api-report Signed-off-by: Samira Mokaram --- packages/theme/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 72d7887214..30dcb7b040 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -64,6 +64,7 @@ export type BackstagePaletteAdditions = { error: string; text: string; link: string; + warning: string; }; }; From 15a205a804d1d4de26f1409d191192f3eb27f57f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 16:18:15 +0200 Subject: [PATCH 228/245] chore: puppeteer! Signed-off-by: blam --- packages/e2e-test/package.json | 7 +++++-- packages/e2e-test/src/commands/run.ts | 14 ++++++++------ packages/e2e-test/src/lib/helpers.ts | 20 +++++++++++--------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index e783fa1de6..06f13d7cbf 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -28,6 +28,7 @@ "@backstage/errors": "^0.1.2", "@types/fs-extra": "^9.0.1", "@types/node": "^14.14.32", + "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", "commander": "^6.1.0", "cross-fetch": "^3.0.6", @@ -35,12 +36,14 @@ "handlebars": "^4.7.3", "pgtools": "^0.3.0", "tree-kill": "^1.2.2", - "ts-node": "^10.0.0", - "zombie": "^6.1.4" + "ts-node": "^10.0.0" }, "nodemonConfig": { "watch": "./src", "exec": "bin/e2e-test", "ext": "ts" + }, + "dependencies": { + "puppeteer": "^10.4.0" } } diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index b0db715f25..b91a65e72e 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -20,7 +20,8 @@ import fetch from 'cross-fetch'; import handlebars from 'handlebars'; import killTree from 'tree-kill'; import { resolve as resolvePath, join as joinPath } from 'path'; -import Browser from 'zombie'; +import puppeteer from 'puppeteer'; + import { spawnPiped, runPlain, @@ -350,17 +351,18 @@ async function testAppServe(pluginName: string, appDir: string) { GITHUB_TOKEN: 'abc', }, }); - Browser.localhost('localhost', 3000); + + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://localhost:3000'); let successful = false; try { for (let attempts = 1; ; attempts++) { try { - const browser = new Browser(); - - await waitForPageWithText(browser, '/', 'My Company Catalog'); + await waitForPageWithText(page, '/', 'My Company Catalog'); await waitForPageWithText( - browser, + page, `/${pluginName}`, `Welcome to ${pluginName}!`, ); diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index 5193db08ec..ddb2714853 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -22,6 +22,7 @@ import { ChildProcess, } from 'child_process'; import { promisify } from 'util'; +import puppeteer from 'puppeteer'; const execFile = promisify(execFileCb); @@ -125,7 +126,7 @@ export async function waitForExit(child: ChildProcess) { } export async function waitForPageWithText( - browser: any, + page: puppeteer.Page, path: string, text: string, { intervalMs = 1000, maxFindAttempts = 50 } = {}, @@ -137,16 +138,17 @@ export async function waitForPageWithText( console.log(`Attempting to load page at ${path}, waiting ${waitTimeMs}`); await new Promise(resolve => setTimeout(resolve, waitTimeMs)); - await browser.visit(path); - - await new Promise(resolve => setTimeout(resolve, waitTimeMs)); + await page.goto(`http://localhost:3000${path}`); const escapedText = text.replace(/"|\\/g, '\\$&'); - browser.assert.evaluate( - `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, - true, - `expected to find text ${text}`, - ); + await expect(() => + page.evaluate(() => + Array.from(document.querySelectorAll('*')).some( + el => el.textContent === escapedText, + ), + ), + ).resolves.toBeTruthy(); + break; } catch (error) { assertError(error); From 1078543bc128b2f875e0b0a77bcf621e499ab095 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 16:20:39 +0200 Subject: [PATCH 229/245] chore: run the lock! Signed-off-by: blam --- yarn.lock | 320 +++++++++++++++++++----------------------------------- 1 file changed, 110 insertions(+), 210 deletions(-) diff --git a/yarn.lock b/yarn.lock index 64f9d5d995..3fec636e32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7464,6 +7464,13 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/puppeteer@^5.4.4": + version "5.4.4" + resolved "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.4.tgz#e92abeccc4f46207c3e1b38934a1246be080ccd0" + integrity sha512-3Nau+qi69CN55VwZb0ATtdUAlYlqOOQ3OfQfq0Hqgc4JMFXiQT/XInlwQ9g6LbicDslE6loIFsXFklGh5XmI6Q== + dependencies: + "@types/node" "*" + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -7973,6 +7980,13 @@ resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== +"@types/yauzl@^2.9.1": + version "2.9.2" + resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" + integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== + dependencies: + "@types/node" "*" + "@types/yup@^0.29.13": version "0.29.13" resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz#21b137ba60841307a3c8a1050d3bf4e63ad561e9" @@ -8397,7 +8411,7 @@ a-sync-waterfall@^1.0.0: resolved "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA== -abab@^2.0.0, abab@^2.0.3: +abab@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== @@ -8427,14 +8441,6 @@ accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-globals@^4.1.0: - version "4.3.4" - resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" - integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -8453,11 +8459,6 @@ acorn-jsx@^5.3.1: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn-walk@^6.0.1: - version "6.2.0" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" - integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== - acorn-walk@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" @@ -8468,12 +8469,7 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^5.5.3: - version "5.7.4" - resolved "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== - -acorn@^6.0.1, acorn@^6.4.1: +acorn@^6.4.1: version "6.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== @@ -9044,11 +9040,6 @@ array-differ@^3.0.0: resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - array-filter@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" @@ -9211,11 +9202,6 @@ async-each@^1.0.1: resolved "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async-lock@^1.1.0: version "1.2.4" resolved "https://registry.npmjs.org/async-lock/-/async-lock-1.2.4.tgz#80d0d612383045dd0c30eb5aad08510c1397cb91" @@ -9646,7 +9632,7 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@6.26.0, babel-runtime@^6.26.0: +babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -9863,7 +9849,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -10132,7 +10118,7 @@ buffer@4.9.2, buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.5.0, buffer@^5.7.0: +buffer@^5.2.1, buffer@^5.5.0, buffer@^5.7.0: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -11953,22 +11939,15 @@ csso@^4.0.2: dependencies: css-tree "1.0.0-alpha.37" -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== -cssstyle@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" - integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== - dependencies: - cssom "0.3.x" +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== cssstyle@^2.2.0: version "2.3.0" @@ -12332,15 +12311,6 @@ dashify@^2.0.0: resolved "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz#fff270ca2868ca427fee571de35691d6e437a648" integrity sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A== -data-urls@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -12687,6 +12657,11 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" +devtools-protocol@0.0.901419: + version "0.0.901419" + resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" + integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ== + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -12881,13 +12856,6 @@ domelementtype@^2.0.1, domelementtype@^2.1.0: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" - domexception@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -13420,7 +13388,7 @@ escape-string-regexp@^5.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -escodegen@^1.14.1, escodegen@^1.9.1: +escodegen@^1.14.1: version "1.14.3" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== @@ -13788,13 +13756,6 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -eventsource@^1.0.5: - version "1.0.7" - resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -14082,6 +14043,17 @@ extract-files@^8.0.0: resolved "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz#46a0690d0fe77411a2e3804852adeaa65cd59288" integrity sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" @@ -15874,13 +15846,6 @@ html-comment-regex@^1.1.0: resolved "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -16104,7 +16069,7 @@ https-browserify@^1.0.0: resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^5.0.0: +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== @@ -16149,7 +16114,7 @@ hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== -iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -17939,38 +17904,6 @@ jscodeshift@^0.13.0: temp "^0.8.4" write-file-atomic "^2.3.0" -jsdom@11.12.0: - version "11.12.0" - resolved "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - jsdom@^16.4.0: version "16.4.0" resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" @@ -18581,11 +18514,6 @@ leasot@^12.0.0: strip-ansi "^6.0.0" text-table "^0.2.0" -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - lerna@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" @@ -20198,7 +20126,7 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.2.0, mime@^2.3.1, mime@^2.4.4, mime@^2.4.6: +mime@^2.2.0, mime@^2.4.4, mime@^2.4.6: version "2.5.2" resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== @@ -21170,7 +21098,7 @@ nunjucks@^3.2.3: asap "^2.0.3" commander "^5.1.0" -nwsapi@^2.0.7, nwsapi@^2.2.0: +nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== @@ -21433,13 +21361,6 @@ ora@^5.3.0, ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -original@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -21878,11 +21799,6 @@ parse-url@^5.0.0: parse-path "^4.0.0" protocols "^1.4.0" -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== - parse5@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" @@ -22364,6 +22280,13 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -22378,13 +22301,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - pkg-dir@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" @@ -22416,11 +22332,6 @@ pluralize@^8.0.0: resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - pnp-webpack-plugin@1.6.4: version "1.6.4" resolved "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" @@ -23017,6 +22928,11 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" + integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== + progress@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -23182,6 +23098,11 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" +proxy-from-env@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -23282,6 +23203,24 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" +puppeteer@^10.4.0: + version "10.4.0" + resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz#a6465ff97fda0576c4ac29601406f67e6fea3dc7" + integrity sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w== + dependencies: + debug "4.3.1" + devtools-protocol "0.0.901419" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.1" + pkg-dir "4.2.0" + progress "2.0.1" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.0.0" + unbzip2-stream "1.3.3" + ws "7.4.6" + q@^1.1.2, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -24587,7 +24526,7 @@ request-promise-core@1.1.3: dependencies: lodash "^4.17.15" -request-promise-native@^1.0.5, request-promise-native@^1.0.8: +request-promise-native@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== @@ -24596,7 +24535,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.87.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -24814,7 +24753,7 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -26585,7 +26524,7 @@ symbol-observable@1.2.0, symbol-observable@^1.0.4, symbol-observable@^1.1.0, sym resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -symbol-tree@^3.2.2, symbol-tree@^3.2.4: +symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== @@ -26628,6 +26567,16 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== +tar-fs@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== + dependencies: + chownr "^1.1.1" + mkdirp "^0.5.1" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -27109,7 +27058,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: +tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -27135,13 +27084,6 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - tr46@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" @@ -27565,6 +27507,14 @@ unbox-primitive@^1.0.0: has-symbols "^1.0.0" which-boxed-primitive "^1.0.1" +unbzip2-stream@1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" + integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -27962,7 +27912,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3, url-parse@^1.5.3: +url-parse@^1.5.3: version "1.5.3" resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== @@ -28291,7 +28241,7 @@ vscode-languageserver-types@^3.15.1: resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== -w3c-hr-time@^1.0.1, w3c-hr-time@^1.0.2: +w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== @@ -28390,11 +28340,6 @@ web-streams-polyfill@4.0.0-beta.1: resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95" integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ== -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -28588,7 +28533,7 @@ websocket-extensions@>=0.1.1: resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: +whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== @@ -28605,29 +28550,11 @@ whatwg-fetch@^3.4.1: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3" integrity sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ== -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: +whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^6.4.1: - version "6.5.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-url@^8.0.0, whatwg-url@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" @@ -28869,25 +28796,16 @@ ws@7.4.5, ws@^7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== -ws@^5.2.0: - version "5.2.3" - resolved "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== - dependencies: - async-limiter "~1.0.0" +ws@7.4.6: + version "7.4.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== "ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1: version "7.5.0" resolved "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== -ws@^6.1.2: - version "6.2.1" - resolved "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - ws@^8.1.0: version "8.2.3" resolved "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" @@ -29260,24 +29178,6 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" -zombie@^6.1.4: - version "6.1.4" - resolved "https://registry.npmjs.org/zombie/-/zombie-6.1.4.tgz#9f0f53f3d9a032beb7f3fe5b382146a3475a4d47" - integrity sha512-yxNvKtyz3PP8lkr31AYh7vdbBD4is9hYXiOQKPp+k/7GiDiFQXX1Ex+peCl4ttodu/bHZcIluJ8lxMla5XefBQ== - dependencies: - babel-runtime "6.26.0" - bluebird "^3.5.1" - debug "^4.1.0" - eventsource "^1.0.5" - iconv-lite "^0.4.21" - jsdom "11.12.0" - lodash "^4.17.10" - mime "^2.3.1" - ms "^2.1.1" - request "^2.85.0" - tough-cookie "^2.3.4" - ws "^6.1.2" - zwitch@^1.0.0: version "1.0.5" resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From e3ef16366edf6f1a9cb47f6472d84b38d4799103 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 16:23:43 +0200 Subject: [PATCH 230/245] chore: setup chrome in the e2e containers Signed-off-by: blam --- .github/workflows/e2e-win.yml | 2 ++ .github/workflows/e2e.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index acaf4e189e..36b3f75578 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -40,6 +40,8 @@ jobs: node-version: ${{ matrix.node-version }} - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v1.0.2 + - name: setup chrome + uses: browser-actions/setup-chrome@latest - name: yarn install run: yarn install --frozen-lockfile diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d7754a0046..0967eeeacc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -53,6 +53,8 @@ jobs: id: yarn-cache if: steps.cache-modules.outputs.cache-hit != 'true' run: echo "::set-output name=dir::$(yarn cache dir)" + - name: setup chrome + uses: browser-actions/setup-chrome@latest - name: cache global yarn cache uses: actions/cache@v2 if: steps.cache-modules.outputs.cache-hit != 'true' From 4b2740332c127ba245df6341ea4e68b9f1f5c6dc Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 18:06:47 +0200 Subject: [PATCH 231/245] chore: now we have puppeteer! Signed-off-by: blam --- packages/e2e-test/package.json | 4 +--- packages/e2e-test/src/commands/run.ts | 11 +++++++---- packages/e2e-test/src/lib/helpers.ts | 14 ++++++++------ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 06f13d7cbf..1b8e321edb 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -35,6 +35,7 @@ "fs-extra": "9.1.0", "handlebars": "^4.7.3", "pgtools": "^0.3.0", + "puppeteer": "^10.4.0", "tree-kill": "^1.2.2", "ts-node": "^10.0.0" }, @@ -42,8 +43,5 @@ "watch": "./src", "exec": "bin/e2e-test", "ext": "ts" - }, - "dependencies": { - "puppeteer": "^10.4.0" } } diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index b91a65e72e..9eb32e378c 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -352,14 +352,16 @@ async function testAppServe(pluginName: string, appDir: string) { }, }); - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - await page.goto('http://localhost:3000'); - let successful = false; + + let browser; try { for (let attempts = 1; ; attempts++) { try { + browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://localhost:3000'); + await waitForPageWithText(page, '/', 'My Company Catalog'); await waitForPageWithText( page, @@ -380,6 +382,7 @@ async function testAppServe(pluginName: string, appDir: string) { } } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes + if (browser) await browser.close(); killTree(startApp.pid); } diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index ddb2714853..b3212f3698 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -141,13 +141,15 @@ export async function waitForPageWithText( await page.goto(`http://localhost:3000${path}`); const escapedText = text.replace(/"|\\/g, '\\$&'); - await expect(() => - page.evaluate(() => - Array.from(document.querySelectorAll('*')).some( - el => el.textContent === escapedText, - ), + const match = await page.evaluate(() => + Array.from(document.querySelectorAll('*')).some( + el => el.textContent === escapedText, ), - ).resolves.toBeTruthy(); + ); + + if (!match) { + throw new Error(`Expected to find text ${escapedText}`); + } break; } catch (error) { From 03f368f740a3a5e1c25c721f43d5ba799c67318b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 18:30:33 +0200 Subject: [PATCH 232/245] chore: finally made it work Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 3 ++- packages/e2e-test/src/lib/helpers.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 9eb32e378c..eedaca9a8a 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -360,7 +360,8 @@ async function testAppServe(pluginName: string, appDir: string) { try { browser = await puppeteer.launch(); const page = await browser.newPage(); - await page.goto('http://localhost:3000'); + + await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' }); await waitForPageWithText(page, '/', 'My Company Catalog'); await waitForPageWithText( diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index b3212f3698..db8861e111 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -138,7 +138,9 @@ export async function waitForPageWithText( console.log(`Attempting to load page at ${path}, waiting ${waitTimeMs}`); await new Promise(resolve => setTimeout(resolve, waitTimeMs)); - await page.goto(`http://localhost:3000${path}`); + await page.goto(`http://localhost:3000${path}`, { + waitUntil: 'networkidle0', + }); const escapedText = text.replace(/"|\\/g, '\\$&'); const match = await page.evaluate(() => From bb5fe02de3d3b08b6eda6ee403248358fe3ead66 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 18:57:42 +0200 Subject: [PATCH 233/245] chore: don't need escaped text now as it's actually a function call like all good javascripts Signed-off-by: blam --- packages/e2e-test/src/lib/helpers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index db8861e111..f51cf5b798 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -142,15 +142,14 @@ export async function waitForPageWithText( waitUntil: 'networkidle0', }); - const escapedText = text.replace(/"|\\/g, '\\$&'); const match = await page.evaluate(() => Array.from(document.querySelectorAll('*')).some( - el => el.textContent === escapedText, + el => el.textContent === text, ), ); if (!match) { - throw new Error(`Expected to find text ${escapedText}`); + throw new Error(`Expected to find text ${text}`); } break; From fc6c990cc9c9ccb34c2438ce33243243d5966c06 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 19:31:53 +0200 Subject: [PATCH 234/245] chore: this time, I promise. I fixed it. Sorted out all the scoping stuff for the hacky stuff that evaluate does with .toString Signed-off-by: blam --- packages/e2e-test/src/lib/helpers.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index f51cf5b798..350898234d 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -142,10 +142,12 @@ export async function waitForPageWithText( waitUntil: 'networkidle0', }); - const match = await page.evaluate(() => - Array.from(document.querySelectorAll('*')).some( - el => el.textContent === text, - ), + const match = await page.evaluate( + textContent => + Array.from(document.querySelectorAll('*')).some( + el => el.textContent === textContent, + ), + text, ); if (!match) { @@ -154,6 +156,7 @@ export async function waitForPageWithText( break; } catch (error) { + console.log(error); assertError(error); findAttempts++; From 614fd12ad0eeb20b130433e726e8e2264f903602 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 20:49:47 +0200 Subject: [PATCH 235/245] chore: wrap up the killTree? Signed-off-by: blam --- packages/core-components/package.json | 4 +- packages/core-components/package.json-prepack | 92 +++++++++++++++++++ packages/e2e-test/src/commands/run.ts | 8 +- 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 packages/core-components/package.json-prepack diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 7f50af7488..5534d27c2a 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -18,8 +18,8 @@ "backstage" ], "license": "Apache-2.0", - "main": "src/index.ts", - "types": "src/index.ts", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", "lint": "backstage-cli lint", diff --git a/packages/core-components/package.json-prepack b/packages/core-components/package.json-prepack new file mode 100644 index 0000000000..038b99ca90 --- /dev/null +++ b/packages/core-components/package.json-prepack @@ -0,0 +1,92 @@ +{ + "name": "@backstage/core-components", + "description": "Core components used by Backstage plugins and apps", + "version": "0.7.1", + "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/core-components" + }, + "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/config": "^0.1.10", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/errors": "^0.1.3", + "@backstage/theme": "^0.2.11", + "@material-table/core": "^3.1.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "@types/react": "*", + "@types/react-sparklines": "^1.7.0", + "@types/react-text-truncate": "^0.14.0", + "classnames": "^2.2.6", + "clsx": "^1.1.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.0.0", + "d3-zoom": "^3.0.0", + "dagre": "^0.8.5", + "immer": "^9.0.1", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "prop-types": "^15.7.2", + "qs": "^6.9.4", + "rc-progress": "^3.0.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-helmet": "6.1.0", + "react-hook-form": "^7.12.2", + "react-markdown": "^7.0.1", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-sparklines": "^1.7.0", + "react-syntax-highlighter": "^15.4.3", + "react-text-truncate": "^0.16.0", + "react-use": "^17.2.4", + "remark-gfm": "^2.0.0", + "zen-observable": "^0.8.15" + }, + "devDependencies": { + "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.0", + "@backstage/test-utils": "^0.1.19", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.1.8", + "@types/classnames": "^2.2.9", + "@types/d3-selection": "^3.0.1", + "@types/d3-shape": "^3.0.1", + "@types/d3-zoom": "^3.0.1", + "@types/dagre": "^0.7.44", + "@types/google-protobuf": "^3.7.2", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/react-helmet": "^6.1.0", + "@types/react-syntax-highlighter": "^13.5.2", + "@types/zen-observable": "^0.8.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index eedaca9a8a..e5399c13f9 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -384,7 +384,9 @@ async function testAppServe(pluginName: string, appDir: string) { } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes if (browser) await browser.close(); - killTree(startApp.pid); + await new Promise((res, rej) => + killTree(startApp.pid, err => (err ? rej(err) : res())), + ); } try { @@ -463,7 +465,9 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { } finally { print('Stopping the child process'); // Kill entire process group, otherwise we'll end up with hanging serve processes - killTree(child.pid); + await new Promise((res, rej) => + killTree(child.pid, err => (err ? rej(err) : res())), + ); } try { From 9e3b76f86a1208837a9363015256447c396beb6c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Oct 2021 21:01:08 +0200 Subject: [PATCH 236/245] chore: revert some accidental additions Signed-off-by: blam i --- packages/core-components/package.json | 4 +- packages/core-components/package.json-prepack | 92 ------------------- 2 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 packages/core-components/package.json-prepack diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 5534d27c2a..7f50af7488 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -18,8 +18,8 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "src/index.ts", + "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", "lint": "backstage-cli lint", diff --git a/packages/core-components/package.json-prepack b/packages/core-components/package.json-prepack deleted file mode 100644 index 038b99ca90..0000000000 --- a/packages/core-components/package.json-prepack +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "@backstage/core-components", - "description": "Core components used by Backstage plugins and apps", - "version": "0.7.1", - "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/core-components" - }, - "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/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", - "@material-table/core": "^3.1.0", - "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", - "@types/react-sparklines": "^1.7.0", - "@types/react-text-truncate": "^0.14.0", - "classnames": "^2.2.6", - "clsx": "^1.1.0", - "d3-selection": "^3.0.0", - "d3-shape": "^3.0.0", - "d3-zoom": "^3.0.0", - "dagre": "^0.8.5", - "immer": "^9.0.1", - "lodash": "^4.17.21", - "pluralize": "^8.0.0", - "prop-types": "^15.7.2", - "qs": "^6.9.4", - "rc-progress": "^3.0.0", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "react-helmet": "6.1.0", - "react-hook-form": "^7.12.2", - "react-markdown": "^7.0.1", - "react-router": "6.0.0-beta.0", - "react-router-dom": "6.0.0-beta.0", - "react-sparklines": "^1.7.0", - "react-syntax-highlighter": "^15.4.3", - "react-text-truncate": "^0.16.0", - "react-use": "^17.2.4", - "remark-gfm": "^2.0.0", - "zen-observable": "^0.8.15" - }, - "devDependencies": { - "@backstage/core-app-api": "^0.1.18", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", - "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react": "^11.2.5", - "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.1.8", - "@types/classnames": "^2.2.9", - "@types/d3-selection": "^3.0.1", - "@types/d3-shape": "^3.0.1", - "@types/d3-zoom": "^3.0.1", - "@types/dagre": "^0.7.44", - "@types/google-protobuf": "^3.7.2", - "@types/jest": "^26.0.7", - "@types/node": "^14.14.32", - "@types/react-helmet": "^6.1.0", - "@types/react-syntax-highlighter": "^13.5.2", - "@types/zen-observable": "^0.8.0" - }, - "files": [ - "dist" - ] -} From 8535bbd53c1061adf145b562b08e7bbd7408dc55 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 28 Oct 2021 21:30:52 +0200 Subject: [PATCH 237/245] Update run.ts Signed-off-by: Ben Lambert --- packages/e2e-test/src/commands/run.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index e5399c13f9..1ea51a9265 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -383,7 +383,6 @@ async function testAppServe(pluginName: string, appDir: string) { } } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes - if (browser) await browser.close(); await new Promise((res, rej) => killTree(startApp.pid, err => (err ? rej(err) : res())), ); From 0f254f45a5489cef9fc38e0584e6ce26dd83db3c Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 Oct 2021 11:04:42 +0200 Subject: [PATCH 238/245] chore: removing postgres env for now Signed-off-by: blam --- .github/workflows/e2e.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0967eeeacc..7ddd8c7ca4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -74,8 +74,4 @@ jobs: run: | sudo sysctl fs.inotify.max_user_watches=524288 yarn e2e-test run - env: - POSTGRES_HOST: localhost - POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + From fbbc73ac7dc385f5c7b2dfecaa48c93ab490fd41 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 Oct 2021 11:42:22 +0200 Subject: [PATCH 239/245] chore: fixing prettier Signed-off-by: blam --- .github/workflows/e2e.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7ddd8c7ca4..287ef91f16 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -74,4 +74,3 @@ jobs: run: | sudo sysctl fs.inotify.max_user_watches=524288 yarn e2e-test run - From 08724f0bc710b1639d91f164e56724f8af393347 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 Oct 2021 18:06:14 +0200 Subject: [PATCH 240/245] chore: actually check the text in stderr to make sure that it's something that we don't expect Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 1ea51a9265..8f4b267b94 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -445,9 +445,26 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { }); let successful = false; + const stdErrorHasErrors = (input: string) => { + const lines = input.split('\n').filter(Boolean); + return ( + lines.filter( + l => + !l.includes('Use of deprecated folder mapping') && + !l.includes('Update this package.json to use a subpath') && + !l.includes( + '(Use `node --trace-deprecation ...` to show where the warning was created)', + ), + ).length !== 0 + ); + }; + try { - await waitFor(() => stdout.includes('Listening on ') || stderr !== ''); + await waitFor( + () => stdout.includes('Listening on ') || stdErrorHasErrors(stderr), + ); if (stderr !== '') { + print(`Expected stderr to be clean, got ${stderr}`); // Skipping the whole block throw new Error(stderr); } @@ -460,6 +477,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { print('Entities fetched successfully'); successful = true; } catch (error) { + print(''); throw new Error(`Backend failed to startup: ${error}`); } finally { print('Stopping the child process'); From 5b43ae3b5e8557f4cd6cf5612789b440df51662d Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 Oct 2021 18:23:41 +0200 Subject: [PATCH 241/245] chore: finally got it working now right? Signed-off-by: blam --- packages/e2e-test/src/commands/run.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 8f4b267b94..e1d8473121 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -377,7 +377,7 @@ async function testAppServe(pluginName: string, appDir: string) { if (attempts >= 20) { throw new Error(`App serve test failed, ${error}`); } - console.log(`App serve failed, trying again, ${error}`); + print(`App serve failed, trying again, ${error}`); await new Promise(resolve => setTimeout(resolve, 1000)); } } @@ -463,7 +463,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { await waitFor( () => stdout.includes('Listening on ') || stdErrorHasErrors(stderr), ); - if (stderr !== '') { + if (stdErrorHasErrors(stderr)) { print(`Expected stderr to be clean, got ${stderr}`); // Skipping the whole block throw new Error(stderr); From 78c512ce8f78b206001df6571be8f9a2bcf534bb Mon Sep 17 00:00:00 2001 From: hrithiksawhney Date: Sat, 30 Oct 2021 22:39:59 +0530 Subject: [PATCH 242/245] added 'Blessed' icons for core plugins Signed-off-by: hrithiksawhney --- .changeset/spicy-panthers-thank.md | 5 +++++ packages/core-app-api/src/app/icons.tsx | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/spicy-panthers-thank.md diff --git a/.changeset/spicy-panthers-thank.md b/.changeset/spicy-panthers-thank.md new file mode 100644 index 0000000000..6c28b002f4 --- /dev/null +++ b/.changeset/spicy-panthers-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +I have added default icons for the catalog, scaffolder, techdocs, and search. diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx index a102026b29..b234a292b1 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/core-app-api/src/app/icons.tsx @@ -18,6 +18,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import MuiApartmentIcon from '@material-ui/icons/Apartment'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; +import MuiSubjectIcon from '@material-ui/icons/Subject'; +import MuiSearchIcon from '@material-ui/icons/Search'; import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; import MuiDocsIcon from '@material-ui/icons/Description'; @@ -35,6 +38,9 @@ import MuiWarningIcon from '@material-ui/icons/Warning'; type AppIconsKey = | 'brokenImage' | 'catalog' + | 'scaffolder' + | 'techdocs' + | 'search' | 'chat' | 'dashboard' | 'docs' @@ -58,6 +64,9 @@ export const defaultAppIcons: AppIcons = { brokenImage: MuiBrokenImageIcon, // 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, From 0f4619cb4db7f4ee4e50087521db7a1399dcb287 Mon Sep 17 00:00:00 2001 From: Hammar Johan Date: Mon, 1 Nov 2021 12:50:42 +0100 Subject: [PATCH 243/245] Add Volvofinans Signed-off-by: Hammar Johan --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index a2015086c3..64bd99e62f 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -62,3 +62,4 @@ | [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | | [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation.| | [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc.| +| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | \ No newline at end of file From 7d95e98682ded152061973a1a336048f4ce97977 Mon Sep 17 00:00:00 2001 From: Tim Jacomb <21194782+timja@users.noreply.github.com> Date: Wed, 3 Nov 2021 14:33:43 +0000 Subject: [PATCH 244/245] Add yarn command for installing jenkins-backend plugin Signed-off-by: Tim Jacomb --- plugins/jenkins-backend/README.md | 6 ++++++ plugins/jenkins/README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/jenkins-backend/README.md b/plugins/jenkins-backend/README.md index bac56b6eaf..56a2a7faab 100644 --- a/plugins/jenkins-backend/README.md +++ b/plugins/jenkins-backend/README.md @@ -12,6 +12,12 @@ This is the backend half of the 2 Jenkins plugins and is responsible for: This plugin needs to be added to an existing backstage instance. +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-jenkins-backend +``` + Typically, this means creating a `src/plugins/jenkins.ts` file and adding a reference to it to `src/index.ts` ### jenkins.ts diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 1af511b16f..2012555d93 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -16,7 +16,7 @@ cd packages/app yarn add @backstage/plugin-jenkins ``` -2. Add and configure the backend plugin according to it's instructions +2. Add and configure the [jenkins-backend](../jenkins-backend) plugin according to it's instructions 3. Add the `EntityJenkinsContent` extension to the `CI/CD` page and `EntityLatestJenkinsRunCard` to the `overview` page in the app (or wherever you'd prefer): From bc4cc0d2bf73e9cf869d00604a13f18c754a6e34 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Wed, 3 Nov 2021 14:56:18 +0000 Subject: [PATCH 245/245] Add index.ts changes Signed-off-by: Tim Jacomb --- plugins/jenkins-backend/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plugins/jenkins-backend/README.md b/plugins/jenkins-backend/README.md index 56a2a7faab..1413a45136 100644 --- a/plugins/jenkins-backend/README.md +++ b/plugins/jenkins-backend/README.md @@ -48,6 +48,29 @@ export default async function createPlugin({ } ``` +### src/index.ts + +```diff +diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts +index f2b14b2..2c64f47 100644 +--- a/packages/backend/src/index.ts ++++ b/packages/backend/src/index.ts +@@ -22,6 +22,7 @@ import { Config } from '@backstage/config'; + import app from './plugins/app'; ++import jenkins from './plugins/jenkins'; + import scaffolder from './plugins/scaffolder'; +@@ -56,6 +57,7 @@ async function main() { + const authEnv = useHotMemoize(module, () => createEnv('auth')); ++ const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); +@@ -63,6 +65,7 @@ async function main() { + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); ++ apiRouter.use('/jenkins', await jenkins(jenkinsEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); +``` + This plugin must be provided with a JenkinsInfoProvider, this is a strategy object for finding the Jenkins instance and job for an entity. There is a standard one provided, but the Integrator is free to build their own.