From 454d17c9902cc2e21dc46be8594cc4830e1e66fd Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 12 Dec 2023 11:45:57 +0100 Subject: [PATCH 01/42] refactor(backend-common/GithubUrlReader): only call fetch inside fetchResponse Signed-off-by: secustor --- .changeset/eight-rice-cross.md | 5 ++ .../src/reading/GithubUrlReader.ts | 55 ++++++++----------- 2 files changed, 28 insertions(+), 32 deletions(-) create mode 100644 .changeset/eight-rice-cross.md diff --git a/.changeset/eight-rice-cross.md b/.changeset/eight-rice-cross.md new file mode 100644 index 0000000000..e40f731a4e --- /dev/null +++ b/.changeset/eight-rice-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Do not call fetch directly but rather use fetchResponse facility diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 764431386d..175815cabf 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -107,7 +107,7 @@ export class GithubUrlReader implements UrlReader { let response: Response; try { - response = await fetch(ghUrl, { + response = await this.fetchResponse(ghUrl, { headers: { ...credentials?.headers, ...(options?.etag && { 'If-None-Match': options.etag }), @@ -125,38 +125,13 @@ export class GithubUrlReader implements UrlReader { signal: options?.signal as any, }); } catch (e) { - throw new Error(`Unable to read ${url}, ${e}`); + throw e; } - if (response.status === 304) { - throw new NotModifiedError(); - } - - if (response.ok) { - return ReadUrlResponseFactory.fromNodeJSReadable(response.body, { - etag: response.headers.get('ETag') ?? undefined, - lastModifiedAt: parseLastModified( - response.headers.get('Last-Modified'), - ), - }); - } - - let message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - throw new NotFoundError(message); - } - - // GitHub returns a 403 response with a couple of headers indicating rate - // limit status. See more in the GitHub docs: - // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting - if ( - response.status === 403 && - response.headers.get('X-RateLimit-Remaining') === '0' - ) { - message += ' (rate limit exceeded)'; - } - - throw new Error(message); + return ReadUrlResponseFactory.fromNodeJSReadable(response.body, { + etag: response.headers.get('ETag') ?? undefined, + lastModifiedAt: parseLastModified(response.headers.get('Last-Modified')), + }); } async readTree( @@ -350,10 +325,26 @@ export class GithubUrlReader implements UrlReader { const response = await fetch(urlAsString, init); if (!response.ok) { - const message = `Request failed for ${urlAsString}, ${response.status} ${response.statusText}`; + let message = `Request failed for ${urlAsString}, ${response.status} ${response.statusText}`; + + if (response.status === 304) { + throw new NotModifiedError(); + } + if (response.status === 404) { throw new NotFoundError(message); } + + // GitHub returns a 403 response with a couple of headers indicating rate + // limit status. See more in the GitHub docs: + // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting + if ( + response.status === 403 && + response.headers.get('X-RateLimit-Remaining') === '0' + ) { + message += ' (rate limit exceeded)'; + } + throw new Error(message); } From 88fcc3b5e56d839b32492ddee45c40fc010514bf Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 12 Dec 2023 11:53:04 +0100 Subject: [PATCH 02/42] docs(backend-common/GithubUrlReader): put fetchResponse in code block Signed-off-by: secustor --- .changeset/eight-rice-cross.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eight-rice-cross.md b/.changeset/eight-rice-cross.md index e40f731a4e..1628276817 100644 --- a/.changeset/eight-rice-cross.md +++ b/.changeset/eight-rice-cross.md @@ -2,4 +2,4 @@ '@backstage/backend-common': patch --- -Do not call fetch directly but rather use fetchResponse facility +Do not call fetch directly but rather use `fetchResponse` facility From 42b4db71ded631e9a286c6b19c6fe16fc211f184 Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 12 Dec 2023 14:37:11 +0100 Subject: [PATCH 03/42] make fetchResponse protected to allow overwriting Signed-off-by: secustor --- packages/backend-common/src/reading/GithubUrlReader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 175815cabf..fed6fd9430 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -317,7 +317,7 @@ export class GithubUrlReader implements UrlReader { return repo.default_branch; } - private async fetchResponse( + protected async fetchResponse( url: string | URL, init: RequestInit, ): Promise { From f4d77420496ff7bb9092a56509bcd643208beac4 Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 14 Dec 2023 15:50:16 +0100 Subject: [PATCH 04/42] remove unnecessary try catch block Signed-off-by: secustor --- .../src/reading/GithubUrlReader.ts | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index fed6fd9430..6fd24afc08 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -105,28 +105,23 @@ export class GithubUrlReader implements UrlReader { credentials, ); - let response: Response; - try { - response = await this.fetchResponse(ghUrl, { - headers: { - ...credentials?.headers, - ...(options?.etag && { 'If-None-Match': options.etag }), - ...(options?.lastModifiedAfter && { - 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), - }), - Accept: 'application/vnd.github.v3.raw', - }, - // TODO(freben): The signal cast is there because pre-3.x versions of - // node-fetch have a very slightly deviating AbortSignal type signature. - // The difference does not affect us in practice however. The cast can - // be removed after we support ESM for CLI dependencies and migrate to - // version 3 of node-fetch. - // https://github.com/backstage/backstage/issues/8242 - signal: options?.signal as any, - }); - } catch (e) { - throw e; - } + const response = await this.fetchResponse(ghUrl, { + headers: { + ...credentials?.headers, + ...(options?.etag && { 'If-None-Match': options.etag }), + ...(options?.lastModifiedAfter && { + 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), + }), + Accept: 'application/vnd.github.v3.raw', + }, + // TODO(freben): The signal cast is there because pre-3.x versions of + // node-fetch have a very slightly deviating AbortSignal type signature. + // The difference does not affect us in practice however. The cast can + // be removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + signal: options?.signal as any, + }); return ReadUrlResponseFactory.fromNodeJSReadable(response.body, { etag: response.headers.get('ETag') ?? undefined, From 36d598bf98ff2389fac3b100d3880c8646b942ad Mon Sep 17 00:00:00 2001 From: Adam Vollrath Date: Tue, 19 Dec 2023 01:18:55 -0600 Subject: [PATCH 05/42] Sanitize input between PR deployment workflows. Signed-off-by: Adam Vollrath --- .github/workflows/uffizzi-preview.yaml | 28 +++++++++----------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index a9cce41fce..79f735c352 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -48,47 +48,37 @@ jobs: let fs = require('fs'); fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data)); - - name: 'Unzip artifact' - run: unzip preview-spec.zip + - name: 'Accept event from first stage' + run: unzip preview-spec.zip event.json - name: Read Event into ENV run: | - echo 'EVENT_JSON<> $GITHUB_ENV - cat event.json >> $GITHUB_ENV - echo -e '\nEOF' >> $GITHUB_ENV + echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_ENV + echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_ENV + echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_ENV - name: Hash Rendered Manifests File id: hash # If the previous workflow was triggered by a PR close event, we will not have a manifests file artifact. - if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + if: ${{ env.ACTION != 'closed' }} run: | + unzip preview-spec.zip manifests.rendered.yml ls echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV - name: Cache Manifests File - if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + if: ${{ env.ACTION != 'closed' }} uses: actions/cache@v3.3.2 with: path: manifests.rendered.yml key: ${{ env.MANIFESTS_FILE_HASH }} - - name: Read PR Number From Event Object - id: pr - run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV - - - name: Read Event Type from Event Object - id: action - run: echo "ACTION=${{ fromJSON(env.EVENT_JSON).action }}" >> $GITHUB_ENV - - - name: Read Git Ref From Event Object - id: ref - run: echo "GIT_REF=${{ fromJSON(env.EVENT_JSON).pull_request.head.sha }}" >> $GITHUB_ENV - - name: DEBUG - Print Job Outputs if: ${{ runner.debug }} run: | echo "PR number: ${{ env.PR_NUMBER }}" echo "Git Ref: ${{ env.GIT_REF }}" + echo "Action: ${{ env.ACTION }}" echo "Manifests file hash: ${{ env.MANIFESTS_FILE_HASH }}" cat event.json From 08faa6ed060da7ca940b9f0534bc2fc6d853b672 Mon Sep 17 00:00:00 2001 From: Adam Vollrath Date: Tue, 19 Dec 2023 12:27:49 -0600 Subject: [PATCH 06/42] Update to store user input in a safer place. Signed-off-by: Adam Vollrath --- .github/workflows/uffizzi-preview.yaml | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index 79f735c352..cd1f6f939c 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} outputs: - manifests-cache-key: ${{ env.MANIFESTS_FILE_HASH }} - git-ref: ${{ env.GIT_REF }} - pr-number: ${{ env.PR_NUMBER }} - action: ${{ env.ACTION }} + manifests-cache-key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} + git-ref: ${{ steps.event.outputs.GIT_REF }} + pr-number: ${{ steps.event.outputs.PR_NUMBER }} + action: ${{ steps.event.outputs.ACTION }} steps: - name: Harden Runner uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 @@ -52,34 +52,35 @@ jobs: run: unzip preview-spec.zip event.json - name: Read Event into ENV + id: event run: | - echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_ENV - echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_ENV - echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_ENV + echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_OUTPUT + echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT + echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT - name: Hash Rendered Manifests File id: hash # If the previous workflow was triggered by a PR close event, we will not have a manifests file artifact. - if: ${{ env.ACTION != 'closed' }} + if: ${{ steps.event.outputs.ACTION != 'closed' }} run: | unzip preview-spec.zip manifests.rendered.yml ls - echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV + echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT - name: Cache Manifests File - if: ${{ env.ACTION != 'closed' }} + if: ${{ steps.event.outputs.ACTION != 'closed' }} uses: actions/cache@v3.3.2 with: path: manifests.rendered.yml - key: ${{ env.MANIFESTS_FILE_HASH }} + key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} - name: DEBUG - Print Job Outputs if: ${{ runner.debug }} run: | - echo "PR number: ${{ env.PR_NUMBER }}" - echo "Git Ref: ${{ env.GIT_REF }}" - echo "Action: ${{ env.ACTION }}" - echo "Manifests file hash: ${{ env.MANIFESTS_FILE_HASH }}" + echo "PR number: ${{ steps.event.outputs.PR_NUMBER }}" + echo "Git Ref: ${{ steps.event.outputs.GIT_REF }}" + echo "Action: ${{ steps.event.outputs.ACTION }}" + echo "Manifests file hash: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }}" cat event.json deploy-uffizzi-preview: @@ -132,7 +133,6 @@ jobs: - name: Fetch cached Manifests File id: cache - # if: ${{ contains(fromJSON('["create", "update"]'), env.UFFIZZI_ACTION) }} uses: actions/cache@v3 with: path: manifests.rendered.yml From c01835945b63e3e49cb34641c9417048343c121f Mon Sep 17 00:00:00 2001 From: Adam Vollrath Date: Thu, 4 Jan 2024 10:59:41 -0600 Subject: [PATCH 07/42] Limit permissions of GHA token in Uffizzi second stage workflow. Signed-off-by: Adam Vollrath --- .github/workflows/uffizzi-preview.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index cd1f6f939c..93c42e6c71 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -12,6 +12,8 @@ jobs: name: Cache Manifests File runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} + permissions: + actions: read outputs: manifests-cache-key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} git-ref: ${{ steps.event.outputs.GIT_REF }} @@ -84,14 +86,14 @@ jobs: cat event.json deploy-uffizzi-preview: - permissions: - contents: read - pull-requests: write - id-token: write name: Deploy to Uffizzi Virtual Cluster needs: - cache-manifests-file if: ${{ github.event.workflow_run.conclusion == 'success' && needs.cache-manifests-file.outputs.action != 'closed' }} + permissions: + contents: read + pull-requests: write + id-token: write runs-on: ubuntu-latest steps: - name: Checkout From 9f4cb8a8f797981b802aee49a5d0190deab5820f Mon Sep 17 00:00:00 2001 From: Adam Vollrath Date: Thu, 4 Jan 2024 11:18:53 -0600 Subject: [PATCH 08/42] Enforce harden-runner policy. Signed-off-by: Adam Vollrath --- .github/workflows/uffizzi-preview.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index 93c42e6c71..c1659097a7 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -13,7 +13,9 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} permissions: - actions: read + # "If you specify the access for any of these scopes, all of those that are not specified are set to none." + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions + actions: read # Access cache outputs: manifests-cache-key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} git-ref: ${{ steps.event.outputs.GIT_REF }} @@ -23,7 +25,10 @@ jobs: - name: Harden Runner uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: - egress-policy: audit + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 - name: 'Download artifacts' # Fetch output (zip archive) from the workflow run that triggered this workflow. From eb81f4228ade0a2b39c5fe458b7a7284e80be63f Mon Sep 17 00:00:00 2001 From: David Festal Date: Wed, 10 Jan 2024 18:05:28 +0100 Subject: [PATCH 09/42] Rename `backend-plugin-manager` package and make it public Signed-off-by: David Festal --- .changeset/clean-mails-bathe.md | 5 ++ .../.eslintrc.js | 0 .../CHANGELOG.md | 2 +- .../README.md | 18 ++--- .../api-report.md | 2 +- .../catalog-info.yaml | 10 +++ .../config.d.ts | 0 .../package.json | 7 +- .../src/__testUtils__/testUtils.ts | 0 .../src/index.ts | 0 .../src/loader/CommonJSModuleLoader.ts | 0 .../src/loader/index.ts | 0 .../src/loader/types.ts | 0 .../src/manager/index.ts | 0 .../src/manager/plugin-manager.test.ts | 2 +- .../src/manager/plugin-manager.ts | 0 .../src/manager/types.ts | 0 .../src/scanner/index.ts | 0 .../scanner/plugin-scanner-watcher.test.ts | 0 .../src/scanner/plugin-scanner.test.ts | 0 .../src/scanner/plugin-scanner.ts | 0 .../src/scanner/types.ts | 0 .../backend-plugin-manager/catalog-info.yaml | 10 --- yarn.lock | 68 +++++++++---------- 24 files changed, 62 insertions(+), 62 deletions(-) create mode 100644 .changeset/clean-mails-bathe.md rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/.eslintrc.js (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/CHANGELOG.md (99%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/README.md (64%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/api-report.md (98%) create mode 100644 packages/backend-dynamic-feature-service/catalog-info.yaml rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/config.d.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/package.json (92%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/__testUtils__/testUtils.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/index.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/loader/CommonJSModuleLoader.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/loader/index.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/loader/types.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/manager/index.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/manager/plugin-manager.test.ts (99%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/manager/plugin-manager.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/manager/types.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/scanner/index.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/scanner/plugin-scanner-watcher.test.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/scanner/plugin-scanner.test.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/scanner/plugin-scanner.ts (100%) rename packages/{backend-plugin-manager => backend-dynamic-feature-service}/src/scanner/types.ts (100%) delete mode 100644 packages/backend-plugin-manager/catalog-info.yaml diff --git a/.changeset/clean-mails-bathe.md b/.changeset/clean-mails-bathe.md new file mode 100644 index 0000000000..c9afc34f90 --- /dev/null +++ b/.changeset/clean-mails-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +--- + +New `backend-dynamic-feature-service` package, for the discovery of dynamic frontend and backend plugins (and modules) and the loading of the backend ones inside the backend application. diff --git a/packages/backend-plugin-manager/.eslintrc.js b/packages/backend-dynamic-feature-service/.eslintrc.js similarity index 100% rename from packages/backend-plugin-manager/.eslintrc.js rename to packages/backend-dynamic-feature-service/.eslintrc.js diff --git a/packages/backend-plugin-manager/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md similarity index 99% rename from packages/backend-plugin-manager/CHANGELOG.md rename to packages/backend-dynamic-feature-service/CHANGELOG.md index 109c2e8409..07b118e771 100644 --- a/packages/backend-plugin-manager/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,4 +1,4 @@ -# @backstage/backend-plugin-manager +# @backstage/backend-dynamic-feature-service ## 0.0.5-next.2 diff --git a/packages/backend-plugin-manager/README.md b/packages/backend-dynamic-feature-service/README.md similarity index 64% rename from packages/backend-plugin-manager/README.md rename to packages/backend-dynamic-feature-service/README.md index c9fd666fb3..211148ee10 100644 --- a/packages/backend-plugin-manager/README.md +++ b/packages/backend-dynamic-feature-service/README.md @@ -1,10 +1,6 @@ -# @backstage/backend-plugin-manager +# @backstage/backend-dynamic-feature-service -This package adds experimental support for **dynamic backend plugins**, according to the content of the proposal in [RFC 18390](https://github.com/backstage/backstage/issues/18390) - -## Status - -**This package is EXPERIMENTAL, and is subject to change according to the discussions and conclusions that happen around the RFC mentioned above.** +This package adds experimental support for **dynamic backend features (plugins and modules)**, according to the content of the proposal in [RFC 18390](https://github.com/backstage/backstage/issues/18390) ## Testing the backend dynamic plugins feature @@ -12,9 +8,9 @@ In order to test the dynamic backend plugins feature provided by this package, e ## How it works -The backend plugin manager is a service that scans a configured root directory (`dynamicPlugins.rootDirectory` in the app config) for dynamic plugin packages, and loads them dynamically. +The dynamic plugin manager is a service that scans a configured root directory (`dynamicPlugins.rootDirectory` in the app config) for dynamic plugin packages, and loads them dynamically. -In the `backend-next` application, it can be enabled by adding the `backend-plugin-manager` as a dependency in the `package.json` and the following lines in the `src/index.ts` file: +In the `backend-next` application, it can be enabled by adding the `backend-dynamic-feature-service` as a dependency in the `package.json` and the following lines in the `src/index.ts` file: ```ts const backend = createBackend(); @@ -36,9 +32,9 @@ Points 2 and 3 can be done by the `export-dynamic-plugin` CLI command used to pe ### About the `export-dynamic-plugin` command -The `export-dynamic-plugin` CLI command, used the dynamic plugin examples provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase), is part of a `@backstage/cli` fork (`@dfatwork-pkgs/backstage-cli@0.22.9-next.6`), and can be used to help packaging the dynamic plugins according to the constraints mentioned above, in order to allow straightforward testing of the dynamic plugins feature. +The `export-dynamic-plugin` CLI command, used the dynamic plugin examples provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase), is part of the `@janus-idp/cli` package, and can be used to help packaging the dynamic plugins according to the constraints mentioned above, in order to allow straightforward testing of the dynamic plugins feature. -However the `backend-plugin-manager` experimental package does **not** depend on the use of this additional CLI command, and in future steps of this backend dynamic plugin work, the use of such a dedicated command might not even be necessary. +However the `backend-dynamic-feature-service` experimental package does **not** depend on the use of this additional CLI command, and in future steps of this backend dynamic plugin work, the use of such a dedicated command might not even be necessary. ### About the support of the legacy backend system @@ -49,4 +45,4 @@ This is why the API related to the old backend is already marked as deprecated. ### Future work -The current implementation of the backend plugin manager is a first step towards the final implementation of the dynamic backend plugins feature, which will be completed / simplified in future steps, as the status of the backstage codebase allows it. +The current implementation of the dynamic plugin manager is a first step towards the final implementation of the dynamic features loading, which will be completed / simplified in future steps, as the status of the backstage codebase allows it. diff --git a/packages/backend-plugin-manager/api-report.md b/packages/backend-dynamic-feature-service/api-report.md similarity index 98% rename from packages/backend-plugin-manager/api-report.md rename to packages/backend-dynamic-feature-service/api-report.md index dc5973a3ab..caf2cf01ec 100644 --- a/packages/backend-plugin-manager/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/backend-plugin-manager" +## API Report File for "@backstage/backend-dynamic-feature-service" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/packages/backend-dynamic-feature-service/catalog-info.yaml b/packages/backend-dynamic-feature-service/catalog-info.yaml new file mode 100644 index 0000000000..1269a5c406 --- /dev/null +++ b/packages/backend-dynamic-feature-service/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-dynamic-feature-service + title: '@backstage/backend-dynamic-feature-service' + description: Backstage backend service to handle dynamic features +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-plugin-manager/config.d.ts b/packages/backend-dynamic-feature-service/config.d.ts similarity index 100% rename from packages/backend-plugin-manager/config.d.ts rename to packages/backend-dynamic-feature-service/config.d.ts diff --git a/packages/backend-plugin-manager/package.json b/packages/backend-dynamic-feature-service/package.json similarity index 92% rename from packages/backend-plugin-manager/package.json rename to packages/backend-dynamic-feature-service/package.json index 45344f4c94..cd95d6a5ae 100644 --- a/packages/backend-plugin-manager/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,8 +1,7 @@ { - "name": "@backstage/backend-plugin-manager", - "description": "Backstage plugin management backend", - "version": "0.0.5-next.2", - "private": true, + "name": "@backstage/backend-dynamic-feature-service", + "description": "Backstage dynamic feature service", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-plugin-manager/src/__testUtils__/testUtils.ts b/packages/backend-dynamic-feature-service/src/__testUtils__/testUtils.ts similarity index 100% rename from packages/backend-plugin-manager/src/__testUtils__/testUtils.ts rename to packages/backend-dynamic-feature-service/src/__testUtils__/testUtils.ts diff --git a/packages/backend-plugin-manager/src/index.ts b/packages/backend-dynamic-feature-service/src/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/index.ts rename to packages/backend-dynamic-feature-service/src/index.ts diff --git a/packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts b/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts similarity index 100% rename from packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts rename to packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts diff --git a/packages/backend-plugin-manager/src/loader/index.ts b/packages/backend-dynamic-feature-service/src/loader/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/loader/index.ts rename to packages/backend-dynamic-feature-service/src/loader/index.ts diff --git a/packages/backend-plugin-manager/src/loader/types.ts b/packages/backend-dynamic-feature-service/src/loader/types.ts similarity index 100% rename from packages/backend-plugin-manager/src/loader/types.ts rename to packages/backend-dynamic-feature-service/src/loader/types.ts diff --git a/packages/backend-plugin-manager/src/manager/index.ts b/packages/backend-dynamic-feature-service/src/manager/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/manager/index.ts rename to packages/backend-dynamic-feature-service/src/manager/index.ts diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts similarity index 99% rename from packages/backend-plugin-manager/src/manager/plugin-manager.test.ts rename to packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts index 405fd629f2..487729687f 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts @@ -47,7 +47,7 @@ import { PluginScanner } from '../scanner/plugin-scanner'; import { findPaths } from '@backstage/cli-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; -describe('backend-plugin-manager', () => { +describe('backend-dynamic-feature-service', () => { const mockDir = createMockDirectory(); describe('loadPlugins', () => { diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts similarity index 100% rename from packages/backend-plugin-manager/src/manager/plugin-manager.ts rename to packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts diff --git a/packages/backend-plugin-manager/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts similarity index 100% rename from packages/backend-plugin-manager/src/manager/types.ts rename to packages/backend-dynamic-feature-service/src/manager/types.ts diff --git a/packages/backend-plugin-manager/src/scanner/index.ts b/packages/backend-dynamic-feature-service/src/scanner/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/index.ts rename to packages/backend-dynamic-feature-service/src/scanner/index.ts diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner-watcher.test.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts rename to packages/backend-dynamic-feature-service/src/scanner/plugin-scanner-watcher.test.ts diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.test.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts rename to packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.test.ts diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/plugin-scanner.ts rename to packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts diff --git a/packages/backend-plugin-manager/src/scanner/types.ts b/packages/backend-dynamic-feature-service/src/scanner/types.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/types.ts rename to packages/backend-dynamic-feature-service/src/scanner/types.ts diff --git a/packages/backend-plugin-manager/catalog-info.yaml b/packages/backend-plugin-manager/catalog-info.yaml deleted file mode 100644 index bc1845fe49..0000000000 --- a/packages/backend-plugin-manager/catalog-info.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: backstage-backend-plugin-manager - title: '@backstage/backend-plugin-manager' - description: Backstage plugin management backend -spec: - lifecycle: experimental - type: backstage-node-library - owner: maintainers diff --git a/yarn.lock b/yarn.lock index 03478d5b50..c77da56725 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3344,6 +3344,40 @@ __metadata: languageName: unknown linkType: soft +"@backstage/backend-dynamic-feature-service@workspace:packages/backend-dynamic-feature-service": + version: 0.0.0-use.local + resolution: "@backstage/backend-dynamic-feature-service@workspace:packages/backend-dynamic-feature-service" + dependencies: + "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-events-backend": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-search-backend-node": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + "@backstage/types": "workspace:^" + "@types/express": ^4.17.6 + chokidar: ^3.5.3 + express: ^4.17.1 + lodash: ^4.17.21 + wait-for-expect: ^3.0.2 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/backend-openapi-utils@workspace:^, @backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils": version: 0.0.0-use.local resolution: "@backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils" @@ -3380,40 +3414,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/backend-plugin-manager@workspace:packages/backend-plugin-manager": - version: 0.0.0-use.local - resolution: "@backstage/backend-plugin-manager@workspace:packages/backend-plugin-manager" - dependencies: - "@backstage/backend-app-api": "workspace:^" - "@backstage/backend-common": "workspace:^" - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-tasks": "workspace:^" - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/config-loader": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/plugin-auth-node": "workspace:^" - "@backstage/plugin-catalog-backend": "workspace:^" - "@backstage/plugin-events-backend": "workspace:^" - "@backstage/plugin-events-node": "workspace:^" - "@backstage/plugin-permission-common": "workspace:^" - "@backstage/plugin-permission-node": "workspace:^" - "@backstage/plugin-scaffolder-node": "workspace:^" - "@backstage/plugin-search-backend-node": "workspace:^" - "@backstage/plugin-search-common": "workspace:^" - "@backstage/types": "workspace:^" - "@types/express": ^4.17.6 - chokidar: ^3.5.3 - express: ^4.17.1 - lodash: ^4.17.21 - wait-for-expect: ^3.0.2 - winston: ^3.2.1 - languageName: unknown - linkType: soft - "@backstage/backend-tasks@workspace:^, @backstage/backend-tasks@workspace:packages/backend-tasks": version: 0.0.0-use.local resolution: "@backstage/backend-tasks@workspace:packages/backend-tasks" From 928efbc54a253d1814c248117051ac27c486daaf Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 13:15:34 -0600 Subject: [PATCH 10/42] Updated auth module default export Signed-off-by: Andre Wanlin --- .changeset/brave-ghosts-pay.md | 5 +++++ .changeset/heavy-moose-reflect.md | 5 +++++ plugins/auth-backend-module-microsoft-provider/src/index.ts | 2 +- plugins/auth-backend-module-pinniped-provider/src/index.ts | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/brave-ghosts-pay.md create mode 100644 .changeset/heavy-moose-reflect.md diff --git a/.changeset/brave-ghosts-pay.md b/.changeset/brave-ghosts-pay.md new file mode 100644 index 0000000000..84d255ea0d --- /dev/null +++ b/.changeset/brave-ghosts-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-pinniped-provider': minor +--- + +**BREAKING** The `authModulePinnipedProvider` is now the default export and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-pinniped-provider'));` diff --git a/.changeset/heavy-moose-reflect.md b/.changeset/heavy-moose-reflect.md new file mode 100644 index 0000000000..639e3c8bf4 --- /dev/null +++ b/.changeset/heavy-moose-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': minor +--- + +**BREAKING** The `authModuleMicrosoftProvider` is now the default export and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider'));` diff --git a/plugins/auth-backend-module-microsoft-provider/src/index.ts b/plugins/auth-backend-module-microsoft-provider/src/index.ts index ae4fbf5926..735b6716c5 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/index.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/index.ts @@ -21,5 +21,5 @@ */ export { microsoftAuthenticator } from './authenticator'; -export { authModuleMicrosoftProvider } from './module'; +export { authModuleMicrosoftProvider as default } from './module'; export { microsoftSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-pinniped-provider/src/index.ts b/plugins/auth-backend-module-pinniped-provider/src/index.ts index df4cd58603..bc2bea4c7e 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/index.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/index.ts @@ -21,4 +21,4 @@ */ export { pinnipedAuthenticator, PinnipedStrategyCache } from './authenticator'; -export { authModulePinnipedProvider } from './module'; +export { authModulePinnipedProvider as default } from './module'; From 25b2d7223813ee23f66528037b4fc1c56c0ff31d Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 14:17:09 -0600 Subject: [PATCH 11/42] Updated so a breaking change is not needed Signed-off-by: Andre Wanlin --- .changeset/brave-ghosts-pay.md | 4 ++-- .changeset/heavy-moose-reflect.md | 4 ++-- .../api-report.md | 4 +++- .../src/deprecated.ts | 23 +++++++++++++++++++ .../src/index.ts | 1 + .../api-report.md | 4 +++- .../src/deprecated.ts | 23 +++++++++++++++++++ .../src/index.ts | 1 + 8 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 plugins/auth-backend-module-microsoft-provider/src/deprecated.ts create mode 100644 plugins/auth-backend-module-pinniped-provider/src/deprecated.ts diff --git a/.changeset/brave-ghosts-pay.md b/.changeset/brave-ghosts-pay.md index 84d255ea0d..d8981fa53b 100644 --- a/.changeset/brave-ghosts-pay.md +++ b/.changeset/brave-ghosts-pay.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-pinniped-provider': minor +'@backstage/plugin-auth-backend-module-pinniped-provider': patch --- -**BREAKING** The `authModulePinnipedProvider` is now the default export and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-pinniped-provider'));` +Deprecated the `authModulePinnipedProvider` export. A default export is now available and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-pinniped-provider'));` diff --git a/.changeset/heavy-moose-reflect.md b/.changeset/heavy-moose-reflect.md index 639e3c8bf4..011d3fc023 100644 --- a/.changeset/heavy-moose-reflect.md +++ b/.changeset/heavy-moose-reflect.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-microsoft-provider': minor +'@backstage/plugin-auth-backend-module-microsoft-provider': patch --- -**BREAKING** The `authModuleMicrosoftProvider` is now the default export and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider'));` +Deprecated the `authModuleMicrosoftProvider` export. A default export is now available and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider'));` diff --git a/plugins/auth-backend-module-microsoft-provider/api-report.md b/plugins/auth-backend-module-microsoft-provider/api-report.md index 21bf77d545..c54b986864 100644 --- a/plugins/auth-backend-module-microsoft-provider/api-report.md +++ b/plugins/auth-backend-module-microsoft-provider/api-report.md @@ -11,7 +11,9 @@ import { PassportProfile } from '@backstage/plugin-auth-node'; import { SignInResolverFactory } from '@backstage/plugin-auth-node'; // @public (undocumented) -export const authModuleMicrosoftProvider: () => BackendFeature; +const authModuleMicrosoftProvider: () => BackendFeature; +export { authModuleMicrosoftProvider }; +export default authModuleMicrosoftProvider; // @public (undocumented) export const microsoftAuthenticator: OAuthAuthenticator< diff --git a/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts b/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts new file mode 100644 index 0000000000..c961e1c942 --- /dev/null +++ b/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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 { authModuleMicrosoftProvider as deprecatedAuthModuleMicrosoftProvider } from './module'; + +/** + * @public + * @deprecated Use default import instead + */ +export { deprecatedAuthModuleMicrosoftProvider as authModuleMicrosoftProvider }; diff --git a/plugins/auth-backend-module-microsoft-provider/src/index.ts b/plugins/auth-backend-module-microsoft-provider/src/index.ts index 735b6716c5..45b7b719cd 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/index.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/index.ts @@ -23,3 +23,4 @@ export { microsoftAuthenticator } from './authenticator'; export { authModuleMicrosoftProvider as default } from './module'; export { microsoftSignInResolvers } from './resolvers'; +export * from './deprecated'; diff --git a/plugins/auth-backend-module-pinniped-provider/api-report.md b/plugins/auth-backend-module-pinniped-provider/api-report.md index 430099713e..3a8b6b5bf6 100644 --- a/plugins/auth-backend-module-pinniped-provider/api-report.md +++ b/plugins/auth-backend-module-pinniped-provider/api-report.md @@ -11,7 +11,9 @@ import { Strategy } from 'openid-client'; import { TokenSet } from 'openid-client'; // @public (undocumented) -export const authModulePinnipedProvider: () => BackendFeature; +const authModulePinnipedProvider: () => BackendFeature; +export { authModulePinnipedProvider }; +export default authModulePinnipedProvider; // @public (undocumented) export const pinnipedAuthenticator: OAuthAuthenticator< diff --git a/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts b/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts new file mode 100644 index 0000000000..f11cdc13e6 --- /dev/null +++ b/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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 { authModulePinnipedProvider as deprecatedAuthModulePinnipedProvider } from './module'; + +/** + * @public + * @deprecated Use default import instead + */ +export { deprecatedAuthModulePinnipedProvider as authModulePinnipedProvider }; diff --git a/plugins/auth-backend-module-pinniped-provider/src/index.ts b/plugins/auth-backend-module-pinniped-provider/src/index.ts index bc2bea4c7e..f4b789644d 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/index.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/index.ts @@ -22,3 +22,4 @@ export { pinnipedAuthenticator, PinnipedStrategyCache } from './authenticator'; export { authModulePinnipedProvider as default } from './module'; +export * from './deprecated'; From e508999a639d7f10d6919e33941e84037f7c15ab Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 10 Jan 2024 16:22:33 -0600 Subject: [PATCH 12/42] Adjustment based on feedback Signed-off-by: Andre Wanlin --- .../auth-backend-module-microsoft-provider/api-report.md | 8 +++++--- .../src/deprecated.ts | 3 ++- .../auth-backend-module-pinniped-provider/api-report.md | 8 +++++--- .../src/deprecated.ts | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/auth-backend-module-microsoft-provider/api-report.md b/plugins/auth-backend-module-microsoft-provider/api-report.md index c54b986864..33600c10d8 100644 --- a/plugins/auth-backend-module-microsoft-provider/api-report.md +++ b/plugins/auth-backend-module-microsoft-provider/api-report.md @@ -10,10 +10,12 @@ import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; import { PassportProfile } from '@backstage/plugin-auth-node'; import { SignInResolverFactory } from '@backstage/plugin-auth-node'; +// @public @deprecated (undocumented) +export const authModuleMicrosoftProvider: () => BackendFeature; + // @public (undocumented) -const authModuleMicrosoftProvider: () => BackendFeature; -export { authModuleMicrosoftProvider }; -export default authModuleMicrosoftProvider; +const authModuleMicrosoftProvider_2: () => BackendFeature; +export default authModuleMicrosoftProvider_2; // @public (undocumented) export const microsoftAuthenticator: OAuthAuthenticator< diff --git a/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts b/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts index c961e1c942..3c671b6c25 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts @@ -20,4 +20,5 @@ import { authModuleMicrosoftProvider as deprecatedAuthModuleMicrosoftProvider } * @public * @deprecated Use default import instead */ -export { deprecatedAuthModuleMicrosoftProvider as authModuleMicrosoftProvider }; +export const authModuleMicrosoftProvider = + deprecatedAuthModuleMicrosoftProvider; diff --git a/plugins/auth-backend-module-pinniped-provider/api-report.md b/plugins/auth-backend-module-pinniped-provider/api-report.md index 3a8b6b5bf6..182ea8ba21 100644 --- a/plugins/auth-backend-module-pinniped-provider/api-report.md +++ b/plugins/auth-backend-module-pinniped-provider/api-report.md @@ -10,10 +10,12 @@ import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; import { Strategy } from 'openid-client'; import { TokenSet } from 'openid-client'; +// @public @deprecated (undocumented) +export const authModulePinnipedProvider: () => BackendFeature; + // @public (undocumented) -const authModulePinnipedProvider: () => BackendFeature; -export { authModulePinnipedProvider }; -export default authModulePinnipedProvider; +const authModulePinnipedProvider_2: () => BackendFeature; +export default authModulePinnipedProvider_2; // @public (undocumented) export const pinnipedAuthenticator: OAuthAuthenticator< diff --git a/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts b/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts index f11cdc13e6..0dd2ed36bd 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts @@ -20,4 +20,4 @@ import { authModulePinnipedProvider as deprecatedAuthModulePinnipedProvider } fr * @public * @deprecated Use default import instead */ -export { deprecatedAuthModulePinnipedProvider as authModulePinnipedProvider }; +export const authModulePinnipedProvider = deprecatedAuthModulePinnipedProvider; From 765af3b39791f2aff77e7a3b4bb60b53635adf1b Mon Sep 17 00:00:00 2001 From: Fer Date: Thu, 21 Dec 2023 11:37:13 -0300 Subject: [PATCH 13/42] Add missing imports on example code Signed-off-by: Fer --- plugins/events-backend/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index 6fa02a7e6a..2fd980f003 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -42,6 +42,7 @@ Add the following to `makeCreateEnv` ```diff // packages/backend/src/index.ts ++ import { DefaultEventBroker } from '@backstage/plugin-events-backend'; + const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); ``` @@ -49,6 +50,7 @@ Then update plugin environment to include the event broker. ```diff // packages/backend/src/types.ts ++ import { EventBroker } from '@backstage/plugin-events-node'; + eventBroker: EventBroker; ``` From 92ea61543164c2978bdc21ac977026c62bde1528 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 10 Jan 2024 11:25:27 +0100 Subject: [PATCH 14/42] chore: added chantgeset Signed-off-by: blam Signed-off-by: blam --- .changeset/ninety-impalas-knock.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ninety-impalas-knock.md diff --git a/.changeset/ninety-impalas-knock.md b/.changeset/ninety-impalas-knock.md new file mode 100644 index 0000000000..78bd55dc1c --- /dev/null +++ b/.changeset/ninety-impalas-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-events-backend': patch +--- + +Update `README.md` From 8ef5595556b39f064393c4d121be483354a774bf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Jan 2024 19:51:59 +0100 Subject: [PATCH 15/42] beps/0001: notifications architecture diagram Signed-off-by: Patrik Oldsberg --- .../notifications-architecture.drawio.svg | 537 ++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 beps/0001-notifications-system/notifications-architecture.drawio.svg diff --git a/beps/0001-notifications-system/notifications-architecture.drawio.svg b/beps/0001-notifications-system/notifications-architecture.drawio.svg new file mode 100644 index 0000000000..9f8194b499 --- /dev/null +++ b/beps/0001-notifications-system/notifications-architecture.drawio.svg @@ -0,0 +1,537 @@ + + + + + + + + + + +
+
+
+ Notification Backend +
+
+
+
+ + Notification Backend + +
+
+ + + + + + + + +
+
+
+ Notification Frontend +
+
+
+
+ + Notification Frontend + +
+
+ + + + +
+
+
+ Signal Backend +
+
+
+
+ + Signal Backend + +
+
+ + + + + + + + + + +
+
+
+ Signal Frontend +
+
+
+
+ + Signal Frontend + +
+
+ + + + + + +
+
+
+ Plugin X Backend +
+
+
+
+ + Plugin X Backend + +
+
+ + + + +
+
+
+ Plugin X Frontend +
+
+
+
+ + Plugin X Frontend + +
+
+ + + + + + + + + + + + +
+
+
+ Event Broker +
+
+
+
+ + Event Broker + +
+
+ + + + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ publish event +
+
+
+
+ + publish event + +
+
+ + + + +
+
+
+ broadcast +
+
+
+
+ + broadcast + +
+
+ + + + +
+
+
+ ??? +
+
+
+
+ + ??? + +
+
+ + + + +
+
+
+ server push +
+
+
+
+ + server push + +
+
+ + + + +
+
+
+ POST +
+
+
+
+ + POST + +
+
+ + + + + + +
+
+
+ External System +
+
+
+
+ + External System + +
+
+ + + + +
+
+
+ email/slack/teams, etc. +
+
+
+
+ + email/slack/teams, etc. + +
+
+ + + + +
+
+
+ Render +
+
+
+
+ + Render + +
+
+ + + + +
+
+
+ + Processor + +
+
+
+
+ + Processor + +
+
+ + + + +
+
+
+ GET +
+
+
+
+ + GET + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 3 +
+
+
+
+ + 3 + +
+
+ + + + +
+
+
+ 1b +
+
+
+
+ + 1b + +
+
+ + + + +
+
+
+ 4 +
+
+
+
+ + 4 + +
+
+ + + + +
+
+
+ 5 +
+
+
+
+ + 5 + +
+
+ + + + +
+
+
+ 6 +
+
+
+
+ + 6 + +
+
+ + + + +
+
+
+ 7b +
+
+
+
+ + 7b + +
+
+ + + + +
+
+
+ 7 +
+
+
+
+ + 7 + +
+
+ + + + + + +
+
+
+ 2 +
+
+
+
+ + 2 + +
+
+ + + + +
+
+
+ Channel Callback +
+
+
+
+ + Channel Callback + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
From a5e6361f302a35c7740c89043aa704637a2d808c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jan 2024 09:46:34 +0100 Subject: [PATCH 16/42] beps: add 0001-notifications-system Signed-off-by: Patrik Oldsberg --- beps/0001-notifications-system/README.md | 255 +++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 beps/0001-notifications-system/README.md diff --git a/beps/0001-notifications-system/README.md b/beps/0001-notifications-system/README.md new file mode 100644 index 0000000000..c7b0ffff64 --- /dev/null +++ b/beps/0001-notifications-system/README.md @@ -0,0 +1,255 @@ +--- +title: Backstage Notifications System +status: provisional +authors: + - Rugvip +project-areas: + - core +creation-date: 2023-01-12 +--- + +# BEP: Backstage Notifications System + +[**Discussion Issue**](https://github.com/backstage/backstage/issues/22213) + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) +- [Design Details](#design-details) +- [Release Plan](#release-plan) +- [Dependencies](#dependencies) +- [Alternatives](#alternatives) + +## Summary + +The Backstage Notifications System provides a way for Backstage plugins to send notifications to users. The notifications are displayed in the Backstage frontend UI, but future extensions can allow them to be sent via external channels such as email. The system includes a push mechanism to make sure that users receive notifications immediately, as well as persistence of notification history and read status. + +## Motivation + +Support for notifications is an old and common features request. See [#639](https://github.com/backstage/backstage/issues/639) and [#10652](https://github.com/backstage/backstage/issues/10652). There are also initiatives to implement notifications in [#20312](https://github.com/backstage/backstage/issues/20312) as well as the underlying infrastructure pieces, for example [#17997](https://github.com/backstage/backstage/issues/17997). + +This proposal assumes that further motivation for a notifications system as a whole is not needed. The focus is instead on the specific goals of an initial notifications system, and generally aiming to address the need to coordinate this work. + +### Goals + +We aim to build a system that would provide support for the following type of notifications, as listed in [#10652](https://github.com/backstage/backstage/issues/10652): + +- System-wide announcements or alerts +- Notifications for component owners (e.g. build failure, successful deployment, new vulnerabilities) +- Notifications for individuals (e.g. updates you have subscribed to, new required training courses) +- Notifications pertaining to a particular entity in the catalog (a given notification might apply to an entity and the owning team) + +Initially we only target a single way to send notifications: via a REST API call to the notifications backend, which in turn also provides an accompanying [backend service](https://backstage.io/docs/backend-system/architecture/services). + +The notifications system will need to support scaled deployments, where multiple backend instances may be deployed. + +In the future we aim to provide an extensible system that can be customized to send notifications to users via other channels. Notifications can be captured and processed in-band in order to trigger custom behavior such as sending an email. The goal is for this to be possible to build, but not necessarily part of the initial implementation. + +### Non-Goals + +The following features are out of scope for this BEP: + +- Building out specific support for channels such as email, Slack, and other external channels. +- Providing a fallback mechanisms when a notification cannot be delivered to a user. For example, if a user has not logged in for a long time, we will not attempt to send an email instead. + +## Proposal + +The notifications system is implemented through two net-new components, the `notifications` plugin, and the `signal` plugin. + +The `notifications` plugin in its simplest form provides the following: + +- A backend API for sending, reading, and interacting with notifications. +- Persistent storage of notification state. +- A frontend UI for viewing notifications. + +The `signal` plugin provides the following: + +- A backend API for pushing lightweight signals to online users. +- A connection between the frontend and backend that can be used to push messages to the client. +- A frontend API that lets plugins to subscribe to specific signals. + +### Signals Plugin + +In the backend the signal plugin implements a general purpose message bus for sending signals from backend plugins to connected users. It relies on the `EventBroker` from the [events plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend/README.md) for the actual message passing in the backend. In order to support scaled deployments, each signal backend instance has a separate subscription to the event broker so that each instance receives all events. It is then up to each backend instance to filter out events that are not relevant to it. For this reason, signals should be kept lightweight and not contain unnecessary data. + +In the frontend the signal plugin has a persistent connection to the signal backend. This is initially implemented as a WebSocket connection, but could in the future also receive fallback mechanisms such as Server Sent Events or long polling. It is important that this connection is authenticated as we will be routing signals to specific users. The exact implementation of the authentication is not part of this proposal, but it should use whatever the outcome of the discussion in issue [#19581](https://github.com/backstage/backstage/issues/19581) is. + +In order to route signals from the sender to the intended receiver the signal plugin uses the concept of signaling channels. They are just like the topics on the message bus, but we use separate terminology in order to avoid confusion. They also sit one layer beneath the even topic in the protocol stack, where the signal channel is communicated as part of the event payload. In the frontend, the signal plugin exposes an API to subscribe to a signal channel. Each time the user receives a signal on the specified channel, a listener is called with the signal payload. + +### Notifications Plugin + +The role of the notifications plugin is to manage the lifecycle of notifications. The backend plugin provides an API for other backends to send notifications, as well as an accompanying [backend service](https://backstage.io/docs/backend-system/architecture/services). It also provides a separate API for the frontend plugin to read notifications for an individual user and manage the read status of notifications. + +The notification backend stores notification using the [database service](https://backstage.io/docs/backend-system/core-services/index#database). In particular it needs to store the following information for each notification: + +- ID +- Payload +- Receivers +- Read status by user + +The receivers is **not** a list of users, but rather a filter that describes who should receive the notification. It must be possible to evaluate this filter in a database query, so that we can efficiently fetch all notifications for a given user. The same filter will also be used by the signal backend to determine which users should receive a signal. + +The notification backend does not provide any new permissions, since creating notifications can only be done by other backend plugins, while reading notifications can only be done by the authenticated user. It is possible that we want to add a permissions for reading notifications, in particular for admin and impersonation use cases, but that is not part of this proposal or the initial implementation. + +The notification frontend plugin provides a UI for viewing notifications, which in the initial implementation can be as simple as needed. The only requirement is that a user is able to view recent notifications and distinguish between read and unread notifications. A notification as marked as read once it has been interacted with. The frontend plugin also subscribes to the notifications signal channel and alerts the user when a new notification is received. + +### Architecture Overview + +The diagram below show an overview of the notifications system architecture and the interaction between the different components. + +![notifications system architecture diagram](./notifications-architecture.drawio.svg) + +The components that are presented with dashed lines are not directly part of this proposal, but included to be considered as future extensions. + +Let's walk through the process of sending of a notification to a user: + +1. A backend plugin that wants to send a notification to a user uses the `NotificationService`, which in turn makes a POST request to the notification backend. The request contains a filter to select the target users, as well as the notification payload. + + 1b. As the notification backend handles the request it could optionally invoke a series of notification processors. These processors can be used to transform the notification payload, send the notification through other channels, and decide whether the regular notification flow should continue or not. This step is not in scope for this proposal. + +2. The notification backend stores the received notification in its database. At this point the notification is available from the notification backend's read API. +3. The notification backend published an event to the event bus on the `'signal'` topic. This event payload contains the signaling channel, in this case `'notifications'`, as well as the target user ID and the notification ID, but not the notification payload. +4. Each instance of the signal backend plugin subscribes to the `'signal'` topic and receives the event. +5. Each signal backend instance has a set of push channels set up to online users. The incoming event is filtered based on the target user ID and the signaling is pushed through all connections with a matching user ID. +6. The signal frontend plugin receives the signal and forwards it to all subscribers of the target channel. In this case, the notifications frontend plugin, who is subscribed to the `'notifications'` channel. +7. The notifications frontend plugin receives the signal with the notification ID. It then makes a request to the notification backend to fetch the notification payload and then displays the notification to the user. + + 7b. Rather than rendering the notification directly, the notifications frontend plugin leaves that to an extension installed by the frontend part of the backend plugin that sent the notification. This allows for a more customized notification experience which can also contain custom actions and data display. This step is not in scope for this proposal. + +## Design Details + +### Backend Services + +The following backend service interfaces are added as part of this proposal. + +#### `NotificationService` + +```ts +// TODO - We may want to add an additional wrapping here with interfaces for Notification and NotificationParameters + +interface SendNotificationRequest { + receivers: SignalReceivers; + + /* TODO - please contribute :) */ +} + +interface SendNotificationResponse { + id: string; + receivers: SignalReceivers; + + /* TODO - please contribute :) */ +} + +interface NotificationService { + sendNotification( + request: SendNotificationRequest, + ): Promise; +} +``` + +#### `SignalService` + +```ts +type SignalReceivers = + | { + type: 'entity'; + entityRef: string; // typically a user or group entity + } + | { + type: 'broadcast'; // all users + }; + +interface Signal { + channel: string; + receivers: SignalReceivers; + payload: JsonObject; +} + +interface SignalService { + signal(signal: Signal): Promise; +} +``` + +### Frontend API + +The following frontend API is added as part of this proposal. + +#### `SignalApi` + +> TODO - we likely need a slightly different approach here, since APIs are lazy loaded. + +```ts +interface SignalSubscriber { + unsubscribe(): void; +} + +interface SignalApi { + subscribe( + channel: string, + listener: (payload: JsonObject) => void, + ): SignalSubscriber; +} +``` + +> TODO: Add payload formats and API details + + + +## Release Plan + +The notification and signal plugins are released as two new plugins in the Backstage ecosystem. They will both start out in an experimental state. + +For the notification plugin to reach a stable release we much reach the following: + +- A stable notifications payload format. +- A stable notifications receiver filter format. +- The event broker must have at least one implementation that supports scaled deployments. + +For the signal plugin to reach a stable release we much reach the following: + +- A stable signal receiver filter format. +- A stable signal channel API in the frontend. + +If any changes are required to the frontend framework to facilitate the implementation of notifications or signals, these will be released as experimental alpha features. They will stay in alpha until they are deemed stable enough, which must happen before a stable release of the notifications system. + +## Dependencies + +Since the signal plugin relies on the event broker for communication, it is a dependency for the notifications system as a whole. The event broker does not currently implement any transport for scaled deployments, which is a requirement for scaled deployments of the notification system. + +## Alternatives + +### Signal Plugin Separation + +One primary consideration for the notifications system was whether the notification plugin should set up its own frontend push connection rather than relying on the signal plugin. The signal plugin separation is done for three primary reasons: + +1. The use-case of sending signals from backend plugins to frontend plugins is not limited to only notifications, since not all signals should necessarily be displayed directly to the user. For example, the catalog backend may want to send a signal to the frontend when an entity is updated so that users that are currently viewing the entity in the frontend can trigger an update of the view. +2. We want to limit the number of WebSocket connections that the frontend needs to maintain. By using a separated signal abstraction it is easier to extend the implementation to support other messaging patterns in the future. For example, we may want to move the Scaffolder logs to be sent via the signal plugin instead. That is much more straightforward with a separate signal plugin compared to adding it to the notifications plugin. +3. The technical implementation of sending signals is sufficiently complex that it makes sense to separate out to its own solution in order to reduce the complexity of the notifications plugin. In particular when it comes to handling scaled deployments. + +### User-to-User Notifications + +The notification backend API for sending notifications is only available to other backend plugins, and is not accessible to end users in the frontend. If this were to be added it would likely be a separate backend module that you can install alongside the notification backend. That is unless the permission framework can be extended to support default policy handlers so that we can reject the permission to send user notifications by default. + +There are a few reasons we don't want to allow user-to-user notifications out of the box: + +- To limit the possibility of spam. Even if well-intended, a notification overload can quickly lead to a bad user experience and users disabling and ignoring notifications. +- From a security perspective it's beneficial to avoid user sent notifications since they could be used to trick users into clicking on malicious links, or other similar attacks. +- Our assumption is that this is a feature that most adopters will not want to use, and so by having it be disabled by default we can avoid additional configuration steps. From e57cc9f7e1ba470ada43a130cafdd5423f43373d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jan 2024 11:41:25 +0100 Subject: [PATCH 17/42] visualizer: rename to app-visualizer and publish Signed-off-by: Patrik Oldsberg --- .changeset/unlucky-bottles-run.md | 5 +++ packages/app-next/package.json | 2 +- packages/app-next/src/App.tsx | 2 +- .../.eslintrc.js | 0 plugins/app-visualizer/README.md | 10 +++++ .../api-report.md | 2 +- .../catalog-info.yaml | 4 +- .../package.json | 11 +++--- .../AppVisualizerPage/AppVisualizerPage.tsx | 0 .../AppVisualizerPage/DetailedVisualizer.tsx | 0 .../AppVisualizerPage/TextVisualizer.tsx | 0 .../AppVisualizerPage/TreeVisualizer.tsx | 0 .../src/components/AppVisualizerPage/index.ts | 0 .../src/index.ts | 0 .../src/plugin.tsx | 8 ++-- plugins/visualizer/CHANGELOG.md | 36 ------------------ plugins/visualizer/README.md | 3 -- yarn.lock | 38 +++++++++---------- 18 files changed, 48 insertions(+), 73 deletions(-) create mode 100644 .changeset/unlucky-bottles-run.md rename plugins/{visualizer => app-visualizer}/.eslintrc.js (100%) create mode 100644 plugins/app-visualizer/README.md rename plugins/{visualizer => app-visualizer}/api-report.md (85%) rename plugins/{visualizer => app-visualizer}/catalog-info.yaml (70%) rename plugins/{visualizer => app-visualizer}/package.json (87%) rename plugins/{visualizer => app-visualizer}/src/components/AppVisualizerPage/AppVisualizerPage.tsx (100%) rename plugins/{visualizer => app-visualizer}/src/components/AppVisualizerPage/DetailedVisualizer.tsx (100%) rename plugins/{visualizer => app-visualizer}/src/components/AppVisualizerPage/TextVisualizer.tsx (100%) rename plugins/{visualizer => app-visualizer}/src/components/AppVisualizerPage/TreeVisualizer.tsx (100%) rename plugins/{visualizer => app-visualizer}/src/components/AppVisualizerPage/index.ts (100%) rename plugins/{visualizer => app-visualizer}/src/index.ts (100%) rename plugins/{visualizer => app-visualizer}/src/plugin.tsx (86%) delete mode 100644 plugins/visualizer/CHANGELOG.md delete mode 100644 plugins/visualizer/README.md diff --git a/.changeset/unlucky-bottles-run.md b/.changeset/unlucky-bottles-run.md new file mode 100644 index 0000000000..63c6ff1249 --- /dev/null +++ b/.changeset/unlucky-bottles-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-visualizer': minor +--- + +Initial release of the app visualizer plugin. diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 4d77e35ad9..fe9b81f6db 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -21,6 +21,7 @@ "@backstage/plugin-airbrake": "workspace:^", "@backstage/plugin-apache-airflow": "workspace:^", "@backstage/plugin-api-docs": "workspace:^", + "@backstage/plugin-app-visualizer": "workspace:^", "@backstage/plugin-azure-devops": "workspace:^", "@backstage/plugin-azure-sites": "workspace:^", "@backstage/plugin-badges": "workspace:^", @@ -74,7 +75,6 @@ "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/plugin-todo": "workspace:^", "@backstage/plugin-user-settings": "workspace:^", - "@backstage/plugin-visualizer": "workspace:^", "@backstage/theme": "workspace:^", "@circleci/backstage-plugin": "^0.1.1", "@material-ui/core": "^4.12.2", diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 747576533a..ec333ce38f 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -32,7 +32,7 @@ import { createExtensionOverrides, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; -import appVisualizerPlugin from '@backstage/plugin-visualizer'; +import appVisualizerPlugin from '@backstage/plugin-app-visualizer'; import { homePage } from './HomePage'; import { convertLegacyApp } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; diff --git a/plugins/visualizer/.eslintrc.js b/plugins/app-visualizer/.eslintrc.js similarity index 100% rename from plugins/visualizer/.eslintrc.js rename to plugins/app-visualizer/.eslintrc.js diff --git a/plugins/app-visualizer/README.md b/plugins/app-visualizer/README.md new file mode 100644 index 0000000000..d297cf3a65 --- /dev/null +++ b/plugins/app-visualizer/README.md @@ -0,0 +1,10 @@ +# @backstage/plugin-app-visualizer + +A plugin to help explore the structure of your Backstage app. + +This plugin providers the following extensions: + +| ID | Type | Description | Default Config | +| ------------------------- | --------- | ------------------------------------ | ------------------------- | +| `page:app-visualizer` | `Page` | The app visualizer page | `{ path: '/visualizer' }` | +| `nav-item:app-visualizer` | `NavItem` | Nav item for the app visualizer page | | diff --git a/plugins/visualizer/api-report.md b/plugins/app-visualizer/api-report.md similarity index 85% rename from plugins/visualizer/api-report.md rename to plugins/app-visualizer/api-report.md index 0cd9eface0..764caa7190 100644 --- a/plugins/visualizer/api-report.md +++ b/plugins/app-visualizer/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-visualizer" +## API Report File for "@backstage/plugin-app-visualizer" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/visualizer/catalog-info.yaml b/plugins/app-visualizer/catalog-info.yaml similarity index 70% rename from plugins/visualizer/catalog-info.yaml rename to plugins/app-visualizer/catalog-info.yaml index f9cedddf87..cb444af524 100644 --- a/plugins/visualizer/catalog-info.yaml +++ b/plugins/app-visualizer/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-plugin-visualizer - title: '@backstage/plugin-visualizer' + name: backstage-plugin-app-visualizer + title: '@backstage/plugin-app-visualizer' description: Visualizes the Backstage app structure spec: lifecycle: experimental diff --git a/plugins/visualizer/package.json b/plugins/app-visualizer/package.json similarity index 87% rename from plugins/visualizer/package.json rename to plugins/app-visualizer/package.json index 8482cca16f..2d220e7633 100644 --- a/plugins/visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,8 +1,7 @@ { - "name": "@backstage/plugin-visualizer", + "name": "@backstage/plugin-app-visualizer", "description": "Visualizes the Backstage app structure", - "private": true, - "version": "0.0.2-next.2", + "version": "0.0.0", "publishConfig": { "access": "public" }, @@ -27,8 +26,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@material-ui/icons": "^4.9.1" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0", @@ -36,7 +34,8 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@types/react": "^16.13.1 || ^17.0.0" }, "files": [ "dist" diff --git a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx similarity index 100% rename from plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx rename to plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx diff --git a/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx similarity index 100% rename from plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx rename to plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx similarity index 100% rename from plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx rename to plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx similarity index 100% rename from plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx rename to plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx diff --git a/plugins/visualizer/src/components/AppVisualizerPage/index.ts b/plugins/app-visualizer/src/components/AppVisualizerPage/index.ts similarity index 100% rename from plugins/visualizer/src/components/AppVisualizerPage/index.ts rename to plugins/app-visualizer/src/components/AppVisualizerPage/index.ts diff --git a/plugins/visualizer/src/index.ts b/plugins/app-visualizer/src/index.ts similarity index 100% rename from plugins/visualizer/src/index.ts rename to plugins/app-visualizer/src/index.ts diff --git a/plugins/visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx similarity index 86% rename from plugins/visualizer/src/plugin.tsx rename to plugins/app-visualizer/src/plugin.tsx index 0aa8053604..9a726ecc99 100644 --- a/plugins/visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -25,14 +25,14 @@ import React from 'react'; const rootRouteRef = createRouteRef(); -const visualizerPage = createPageExtension({ +const appVisualizerPage = createPageExtension({ defaultPath: '/visualizer', routeRef: rootRouteRef, loader: () => import('./components/AppVisualizerPage').then(m => ), }); -export const visualizerNavItem = createNavItemExtension({ +export const appVisualizerNavItem = createNavItemExtension({ title: 'Visualizer', icon: VisualizerIcon, routeRef: rootRouteRef, @@ -40,6 +40,6 @@ export const visualizerNavItem = createNavItemExtension({ /** @public */ export const visualizerPlugin = createPlugin({ - id: 'visualizer', - extensions: [visualizerPage, visualizerNavItem], + id: 'app-visualizer', + extensions: [appVisualizerPage, appVisualizerNavItem], }); diff --git a/plugins/visualizer/CHANGELOG.md b/plugins/visualizer/CHANGELOG.md deleted file mode 100644 index 824b909935..0000000000 --- a/plugins/visualizer/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -# @backstage/plugin-visualizer - -## 0.0.2-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/frontend-plugin-api@0.4.1-next.2 - -## 0.0.2-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.2-next.0 - - @backstage/core-components@0.13.10-next.1 - - @backstage/frontend-plugin-api@0.4.1-next.1 - -## 0.0.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.13.10-next.0 - - @backstage/frontend-plugin-api@0.4.1-next.0 - - @backstage/core-plugin-api@1.8.1 - -## 0.0.1 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.1 - - @backstage/frontend-plugin-api@0.4.0 - - @backstage/core-components@0.13.9 - - @backstage/theme@0.5.0 diff --git a/plugins/visualizer/README.md b/plugins/visualizer/README.md deleted file mode 100644 index dd6f081a18..0000000000 --- a/plugins/visualizer/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @backstage/plugin-visualizer - -A plugin to help visualize the structure of your Backstage app. diff --git a/yarn.lock b/yarn.lock index edb50887c1..230f6d3e8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4644,6 +4644,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-app-visualizer@workspace:^, @backstage/plugin-app-visualizer@workspace:plugins/app-visualizer": + version: 0.0.0-use.local + resolution: "@backstage/plugin-app-visualizer@workspace:plugins/app-visualizer" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-atlassian-provider@workspace:^, @backstage/plugin-auth-backend-module-atlassian-provider@workspace:plugins/auth-backend-module-atlassian-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-atlassian-provider@workspace:plugins/auth-backend-module-atlassian-provider" @@ -9563,24 +9581,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-visualizer@workspace:^, @backstage/plugin-visualizer@workspace:plugins/visualizer": - version: 0.0.0-use.local - resolution: "@backstage/plugin-visualizer@workspace:plugins/visualizer" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/core-components": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" - "@backstage/frontend-plugin-api": "workspace:^" - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@types/react": ^16.13.1 || ^17.0.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - languageName: unknown - linkType: soft - "@backstage/plugin-xcmetrics@workspace:plugins/xcmetrics": version: 0.0.0-use.local resolution: "@backstage/plugin-xcmetrics@workspace:plugins/xcmetrics" @@ -26383,6 +26383,7 @@ __metadata: "@backstage/plugin-airbrake": "workspace:^" "@backstage/plugin-apache-airflow": "workspace:^" "@backstage/plugin-api-docs": "workspace:^" + "@backstage/plugin-app-visualizer": "workspace:^" "@backstage/plugin-azure-devops": "workspace:^" "@backstage/plugin-azure-sites": "workspace:^" "@backstage/plugin-badges": "workspace:^" @@ -26436,7 +26437,6 @@ __metadata: "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/plugin-todo": "workspace:^" "@backstage/plugin-user-settings": "workspace:^" - "@backstage/plugin-visualizer": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@circleci/backstage-plugin": ^0.1.1 From a0b70151e6dd29644e091bfb195c1cbb111231f9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jan 2024 15:51:33 +0100 Subject: [PATCH 18/42] Update plugins/app-visualizer/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- plugins/app-visualizer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/app-visualizer/README.md b/plugins/app-visualizer/README.md index d297cf3a65..865cd66746 100644 --- a/plugins/app-visualizer/README.md +++ b/plugins/app-visualizer/README.md @@ -2,7 +2,7 @@ A plugin to help explore the structure of your Backstage app. -This plugin providers the following extensions: +This plugin provides the following extensions: | ID | Type | Description | Default Config | | ------------------------- | --------- | ------------------------------------ | ------------------------- | From 7e7319bcb00d4c6f9ce4ffffd2234727ad1e17c7 Mon Sep 17 00:00:00 2001 From: Daniel Laird Date: Fri, 12 Jan 2024 15:31:56 +0000 Subject: [PATCH 19/42] Ensure teamReviewer list contains unique team names before sending to API Signed-off-by: Daniel Laird --- .../src/actions/githubPullRequest.test.ts | 4 ++-- .../src/actions/githubPullRequest.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts index 23ac4bda45..c5b1038a01 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -377,7 +377,7 @@ describe('createPublishGithubPullRequestAction', () => { branchName: 'new-app', description: 'This PR is really good', reviewers: ['foobar'], - teamReviewers: ['team-foo'], + teamReviewers: ['team-foo', 'team-foo', 'team-bar'], }; mockDir.setContent({ [workspacePath]: {} }); @@ -401,7 +401,7 @@ describe('createPublishGithubPullRequestAction', () => { repo: 'myrepo', pull_number: 123, reviewers: ['foobar'], - team_reviewers: ['team-foo'], + team_reviewers: ['team-foo', 'team-bar'], }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 0763d4a474..2d8dc7ce8d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -372,7 +372,7 @@ export const createPublishGithubPullRequestAction = ( repo: pr.repo, pull_number: pr.number, reviewers, - team_reviewers: teamReviewers, + team_reviewers: teamReviewers ? [...new Set(teamReviewers)] : undefined, }); const addedUsers = result.data.requested_reviewers?.join(', ') ?? ''; const addedTeams = result.data.requested_teams?.join(', ') ?? ''; From 547030034df4d263a28fe423b3e7a9db2ddf9b38 Mon Sep 17 00:00:00 2001 From: Daniel Laird Date: Fri, 12 Jan 2024 15:33:29 +0000 Subject: [PATCH 20/42] Add Changeset Signed-off-by: Daniel Laird --- .changeset/fair-spies-rescue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fair-spies-rescue.md diff --git a/.changeset/fair-spies-rescue.md b/.changeset/fair-spies-rescue.md new file mode 100644 index 0000000000..2b49d4b969 --- /dev/null +++ b/.changeset/fair-spies-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': minor +--- + +Ensure `teamReviewers` list is unique before calling API From 074dfe37b04c9c693d432082176642e0f7ed051b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jan 2024 17:11:34 +0100 Subject: [PATCH 21/42] frontend-app-api: switch undeclared inputs to be a warning instead of error Signed-off-by: Patrik Oldsberg --- .changeset/silver-countries-notice.md | 5 ++++ .../src/tree/instantiateAppNodeTree.test.ts | 24 +++++++++------ .../src/tree/instantiateAppNodeTree.ts | 30 +++++++++---------- 3 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 .changeset/silver-countries-notice.md diff --git a/.changeset/silver-countries-notice.md b/.changeset/silver-countries-notice.md new file mode 100644 index 0000000000..457042487b --- /dev/null +++ b/.changeset/silver-countries-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +Attaching extensions to an input that does not exist is now a warning rather than an error. diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 7855b87df2..ec8712cf59 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -30,6 +30,7 @@ import { AppNodeSpec } from '@backstage/frontend-plugin-api'; import { resolveAppTree } from './resolveAppTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { withLogCollector } from '@backstage/test-utils'; const testDataRef = createExtensionDataRef('test'); const otherDataRef = createExtensionDataRef('other'); @@ -433,8 +434,8 @@ describe('createAppNodeInstance', () => { ); }); - it('should refuse to create an instance with undeclared inputs', () => { - expect(() => + it('should warn when creating an instance with undeclared inputs', () => { + const { warn } = withLogCollector(['warn'], () => createAppNodeInstance({ attachments: new Map([ [ @@ -458,7 +459,7 @@ describe('createAppNodeInstance', () => { resolveExtensionDefinition( createExtension({ namespace: 'app', - name: 'test', + name: 'parent', attachTo: { id: 'ignored', input: 'ignored' }, inputs: { declared: createExtensionInput({ @@ -471,13 +472,15 @@ describe('createAppNodeInstance', () => { ), ), }), - ).toThrow( - "Failed to instantiate extension 'app/test', received undeclared input 'undeclared' from extension 'app/test'", ); + + expect(warn).toEqual([ + "The extension 'app/test' is attached to the input 'undeclared' of 'app/parent', but the extension 'app/parent' noes not declare a 'undeclared' input", + ]); }); it('should refuse to create an instance with multiple undeclared inputs', () => { - expect(() => + const { warn } = withLogCollector(['warn'], () => createAppNodeInstance({ attachments: new Map([ [ @@ -496,7 +499,7 @@ describe('createAppNodeInstance', () => { resolveExtensionDefinition( createExtension({ namespace: 'app', - name: 'test', + name: 'parent', attachTo: { id: 'ignored', input: 'ignored' }, output: {}, factory: () => ({}), @@ -504,9 +507,12 @@ describe('createAppNodeInstance', () => { ), ), }), - ).toThrow( - "Failed to instantiate extension 'app/test', received undeclared inputs 'undeclared1' from extension 'app/test' and 'undeclared2' from extensions 'app/test', 'app/test'", ); + + expect(warn).toEqual([ + "The extension 'app/test' is attached to the input 'undeclared1' of 'app/parent', but the extension 'app/parent' noes not declare a 'undeclared1' input", + "The extensions 'app/test', 'app/test' are attached to the input 'undeclared2' of 'app/parent', but the extension 'app/parent' noes not declare a 'undeclared2' input", + ]); }); it('should refuse to create an instance with multiple inputs for required singleton', () => { diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index f72fcdd0cb..b1fa9eae39 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -46,26 +46,26 @@ function resolveInputData( } function resolveInputs( + id: string, inputMap: AnyExtensionInputMap, attachments: ReadonlyMap, ): ResolvedExtensionInputs { const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, ); - // TODO: Make this a warning rather than an error - if (undeclaredAttachments.length > 0) { - throw new Error( - `received undeclared input${ - undeclaredAttachments.length > 1 ? 's' : '' - } ${undeclaredAttachments - .map( - ([k, exts]) => - `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts - .map(e => e.spec.id) - .join("', '")}'`, - ) - .join(' and ')}`, - ); + + if (process.env.NODE_ENV !== 'production') { + for (const [name, nodes] of undeclaredAttachments) { + const pl = nodes.length > 1; + // eslint-disable-next-line no-console + console.warn( + `The extension${pl ? 's' : ''} '${nodes + .map(n => n.spec.id) + .join("', '")}' ${ + pl ? 'are' : 'is' + } attached to the input '${name}' of '${id}', but the extension '${id}' noes not declare a '${name}' input`, + ); + } } return mapValues(inputMap, (input, inputName) => { @@ -129,7 +129,7 @@ export function createAppNodeInstance(options: { const namedOutputs = internalExtension.factory({ node, config: parsedConfig, - inputs: resolveInputs(internalExtension.inputs, attachments), + inputs: resolveInputs(id, internalExtension.inputs, attachments), }); for (const [name, output] of Object.entries(namedOutputs)) { From 4780af8e153de5686c61b3cda58da5c7b73d227f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Jan 2024 17:49:22 +0100 Subject: [PATCH 22/42] frontend-app-api: list candidate inputs in undeclared input warning Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.test.ts | 6 +++--- .../src/tree/instantiateAppNodeTree.ts | 16 +++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index ec8712cf59..766cb4804e 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -475,7 +475,7 @@ describe('createAppNodeInstance', () => { ); expect(warn).toEqual([ - "The extension 'app/test' is attached to the input 'undeclared' of 'app/parent', but the extension 'app/parent' noes not declare a 'undeclared' input", + "The extension 'app/test' is attached to the input 'undeclared' of the extension 'app/parent', but it has no such input (candidates are 'declared')", ]); }); @@ -510,8 +510,8 @@ describe('createAppNodeInstance', () => { ); expect(warn).toEqual([ - "The extension 'app/test' is attached to the input 'undeclared1' of 'app/parent', but the extension 'app/parent' noes not declare a 'undeclared1' input", - "The extensions 'app/test', 'app/test' are attached to the input 'undeclared2' of 'app/parent', but the extension 'app/parent' noes not declare a 'undeclared2' input", + "The extension 'app/test' is attached to the input 'undeclared1' of the extension 'app/parent', but it has no inputs", + "The extensions 'app/test', 'app/test' are attached to the input 'undeclared2' of the extension 'app/parent', but it has no inputs", ]); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index b1fa9eae39..90a256fea6 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -55,15 +55,21 @@ function resolveInputs( ); if (process.env.NODE_ENV !== 'production') { + const inputNames = Object.keys(inputMap); + for (const [name, nodes] of undeclaredAttachments) { const pl = nodes.length > 1; // eslint-disable-next-line no-console console.warn( - `The extension${pl ? 's' : ''} '${nodes - .map(n => n.spec.id) - .join("', '")}' ${ - pl ? 'are' : 'is' - } attached to the input '${name}' of '${id}', but the extension '${id}' noes not declare a '${name}' input`, + [ + `The extension${pl ? 's' : ''} '${nodes + .map(n => n.spec.id) + .join("', '")}' ${pl ? 'are' : 'is'}`, + `attached to the input '${name}' of the extension '${id}', but it`, + inputNames.length === 0 + ? 'has no inputs' + : `has no such input (candidates are '${inputNames.join("', '")}')`, + ].join(' '), ); } } From c3249d6c11569d086f199db65880f58592cdfb54 Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 12 Jan 2024 11:31:19 -0600 Subject: [PATCH 23/42] Add code to handle URL Reader from GCS with wildcard \* Signed-off-by: armandocomellas1 --- .changeset/thirty-cats-help.md | 5 +++++ .../catalog-backend/src/modules/core/UrlReaderProcessor.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/thirty-cats-help.md diff --git a/.changeset/thirty-cats-help.md b/.changeset/thirty-cats-help.md new file mode 100644 index 0000000000..70ae78e3c1 --- /dev/null +++ b/.changeset/thirty-cats-help.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Change script in **UrlReaderProcessor.ts** Replacing the line code 127 'const { pathname: filepath } = new URL(location)' with to handle URL Reader from GCS with wildcard \* diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index 5e31dcbd1a..c568f03a81 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -17,7 +17,6 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; -import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import { LocationSpec } from '@backstage/plugin-catalog-common'; @@ -124,7 +123,8 @@ export class UrlReaderProcessor implements CatalogProcessor { ): Promise<{ response: { data: Buffer; url: string }[]; etag?: string }> { // Does it contain globs? I.e. does it contain asterisks or question marks // (no curly braces for now) - const { filepath } = parseGitUrl(location); + + const { pathname: filepath } = new URL(location); if (filepath?.match(/[*?]/)) { const limiter = limiterFactory(5); const response = await this.options.reader.search(location, { etag }); From 41b25258bff4869bf6394d7cf353764e1cf7d558 Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 12 Jan 2024 11:36:42 -0600 Subject: [PATCH 24/42] Fixing the linter error with the naming the variable inside the changeset thirty-cats-help.md Signed-off-by: armandocomellas1 --- .changeset/thirty-cats-help.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-cats-help.md b/.changeset/thirty-cats-help.md index 70ae78e3c1..107fb7584b 100644 --- a/.changeset/thirty-cats-help.md +++ b/.changeset/thirty-cats-help.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Change script in **UrlReaderProcessor.ts** Replacing the line code 127 'const { pathname: filepath } = new URL(location)' with to handle URL Reader from GCS with wildcard \* +Change script in **UrlReaderProcessor.ts** Replacing the line code 127 **const { pathname: filepath } = new URL(location)** with to handle URL Reader from GCS with wildcard \* From 1de52a9c60d6ba373a8c9f1b2cbb05f198f957fa Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Fri, 12 Jan 2024 11:40:45 -0600 Subject: [PATCH 25/42] Fixing the linter error with the naming the variable inside the changeset thirty-cats-help.md Signed-off-by: armandocomellas1 --- .changeset/thirty-cats-help.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-cats-help.md b/.changeset/thirty-cats-help.md index 107fb7584b..bb6011ca9c 100644 --- a/.changeset/thirty-cats-help.md +++ b/.changeset/thirty-cats-help.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Change script in **UrlReaderProcessor.ts** Replacing the line code 127 **const { pathname: filepath } = new URL(location)** with to handle URL Reader from GCS with wildcard \* +Change script in **UrlReaderProcessor.ts** Replacing the line code 127 with method (new URL) to handle URL Reader from GCS with wildcard \* From b6b11677582ffe03c5df05b8e9130d2e2c62d96b Mon Sep 17 00:00:00 2001 From: secustor Date: Fri, 12 Jan 2024 20:05:19 +0100 Subject: [PATCH 26/42] chore: revert exposing of fetchResponse Signed-off-by: secustor --- packages/backend-common/src/reading/GithubUrlReader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 6fd24afc08..67f6a975c1 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -312,7 +312,7 @@ export class GithubUrlReader implements UrlReader { return repo.default_branch; } - protected async fetchResponse( + private async fetchResponse( url: string | URL, init: RequestInit, ): Promise { From c97fa1c2bd3bae393f96054f25e8450ab1be4a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Jan 2024 13:56:58 +0100 Subject: [PATCH 27/42] add elements and wrappers to the app root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/silent-horses-raise.md | 7 ++ .../src/extensions/AppRoot.tsx | 40 ++++-- packages/frontend-plugin-api/api-report.md | 56 +++++++++ .../createAppRootElementExtension.test.tsx | 107 ++++++++++++++++ .../createAppRootElementExtension.ts | 71 +++++++++++ .../createAppRootWrapperExtension.test.tsx | 119 ++++++++++++++++++ .../createAppRootWrapperExtension.tsx | 84 +++++++++++++ .../src/extensions/index.ts | 2 + .../src/app/createExtensionTester.tsx | 49 ++++++-- 9 files changed, 514 insertions(+), 21 deletions(-) create mode 100644 .changeset/silent-horses-raise.md create mode 100644 packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts create mode 100644 packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx diff --git a/.changeset/silent-horses-raise.md b/.changeset/silent-horses-raise.md new file mode 100644 index 0000000000..de2671c3da --- /dev/null +++ b/.changeset/silent-horses-raise.md @@ -0,0 +1,7 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +'@backstage/frontend-app-api': patch +--- + +Added `elements` and `wrappers` inputs to `app/root`, that let you add things to the root of the React tree above the layout. You can use the `createAppRootElementExtension` and `createAppRootWrapperExtension` extension creator, respectively, to conveniently create such extensions. diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index 0894287541..b442533ee6 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -14,9 +14,16 @@ * limitations under the License. */ -import React, { ComponentType, ReactNode, useContext, useState } from 'react'; +import React, { + ComponentType, + Fragment, + ReactNode, + useContext, + useState, +} from 'react'; import { coreExtensionData, + createAppRootWrapperExtension, createExtension, createExtensionInput, createSignInPageExtension, @@ -40,26 +47,43 @@ export const AppRoot = createExtension({ attachTo: { id: 'app', input: 'root' }, inputs: { signInPage: createExtensionInput( - { - component: createSignInPageExtension.componentDataRef, - }, + { component: createSignInPageExtension.componentDataRef }, { singleton: true, optional: true }, ), children: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, + { element: coreExtensionData.reactElement }, { singleton: true }, ), + elements: createExtensionInput( + { element: coreExtensionData.reactElement }, + { optional: true }, + ), + wrappers: createExtensionInput( + { component: createAppRootWrapperExtension.componentDataRef }, + { optional: true }, + ), }, output: { element: coreExtensionData.reactElement, }, factory({ inputs }) { + let content: React.ReactNode = ( + <> + {inputs.elements.map(el => ( + {el.output.element} + ))} + {inputs.children.output.element} + + ); + + for (const wrapper of inputs.wrappers) { + content = {content}; + } + return { element: ( - {inputs.children.output.element} + {content} ), }; diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 2260db334f..6113c946ee 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -67,6 +67,7 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -390,6 +391,61 @@ export { createApiFactory }; export { createApiRef }; +// @public +export function createAppRootElementExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + element: + | JSX_2.Element + | ((options: { + inputs: Expand>; + config: TConfig; + }) => JSX_2.Element); +}): ExtensionDefinition; + +// @public +export function createAppRootWrapperExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + Component: ComponentType< + PropsWithChildren<{ + inputs: Expand>; + config: TConfig; + }> + >; +}): ExtensionDefinition; + +// @public (undocumented) +export namespace createAppRootWrapperExtension { + const // (undocumented) + componentDataRef: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + {} + >; +} + // @public (undocumented) export function createComponentExtension< TProps extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx new file mode 100644 index 0000000000..c8bf6ee355 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2023 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { createSchemaFromZod } from '../schema/createSchemaFromZod'; +import { coreExtensionData } from '../wiring/coreExtensionData'; +import { createExtension } from '../wiring/createExtension'; +import { createExtensionInput } from '../wiring/createExtensionInput'; +import { createAppRootElementExtension } from './createAppRootElementExtension'; + +describe('createAppRootElementExtension', () => { + it('works with simple options and just an element', async () => { + const extension = createAppRootElementExtension({ + element:
Hello
, + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-root-element', + attachTo: { id: 'app/root', input: 'elements' }, + disabled: false, + inputs: {}, + output: { + element: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + createExtensionTester(extension).render(); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + }); + + it('works with complex options and a callback', async () => { + const schema = createSchemaFromZod(z => z.object({ name: z.string() })); + + const extension = createAppRootElementExtension({ + namespace: 'ns', + name: 'test', + configSchema: schema, + attachTo: { id: 'other', input: 'slot' }, + disabled: true, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + element: ({ inputs, config }) => ( +
+ Hello, {config.name}, {inputs.children.length} +
+ ), + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-root-element', + namespace: 'ns', + name: 'test', + attachTo: { id: 'other', input: 'slot' }, + configSchema: schema, + disabled: true, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + element: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + createExtensionTester(extension, { config: { name: 'Robin' } }) + .add( + createExtension({ + attachTo: { id: 'app-root-element:ns/test', input: 'children' }, + output: { element: coreExtensionData.reactElement }, + factory: () => ({ element:
}), + }), + ) + .render(); + + await expect( + screen.findByText('Hello, Robin, 1'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts new file mode 100644 index 0000000000..8be82d5540 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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 { JSX } from 'react'; +import { PortableSchema } from '../schema/types'; +import { Expand } from '../types'; +import { coreExtensionData } from '../wiring/coreExtensionData'; +import { + AnyExtensionInputMap, + ExtensionDefinition, + ResolvedExtensionInputs, + createExtension, +} from '../wiring/createExtension'; + +/** + * Creates an extension that renders a React element at the app root, outside of + * the app layout. This is useful for example for shared popups and similar. + * + * @public + */ +export function createAppRootElementExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + element: + | JSX.Element + | ((options: { + inputs: Expand>; + config: TConfig; + }) => JSX.Element); +}): ExtensionDefinition { + return createExtension({ + kind: 'app-root-element', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/root', input: 'elements' }, + configSchema: options.configSchema, + disabled: options.disabled, + inputs: options.inputs, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs, config }) { + return { + element: + typeof options.element === 'function' + ? options.element({ inputs, config }) + : options.element, + }; + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx new file mode 100644 index 0000000000..a0625ec1ae --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2023 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { createSchemaFromZod } from '../schema/createSchemaFromZod'; +import { coreExtensionData } from '../wiring/coreExtensionData'; +import { createExtension } from '../wiring/createExtension'; +import { createExtensionInput } from '../wiring/createExtensionInput'; +import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; +import { createPageExtension } from './createPageExtension'; + +describe('createAppRootWrapperExtension', () => { + it('works with simple options and no props', async () => { + const extension = createAppRootWrapperExtension({ + Component: () =>
Hello
, + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-wrapper-component', + attachTo: { id: 'app/root', input: 'wrappers' }, + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + createExtensionTester( + createPageExtension({ + defaultPath: '/', + loader: async () =>
, + }), + ) + .add(extension) + .render(); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + }); + + it('works with complex options and props', async () => { + const schema = createSchemaFromZod(z => z.object({ name: z.string() })); + + const extension = createAppRootWrapperExtension({ + namespace: 'ns', + name: 'test', + configSchema: schema, + disabled: true, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + Component: ({ inputs, config, children }) => ( +
+ {children} +
+ ), + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-wrapper-component', + namespace: 'ns', + name: 'test', + attachTo: { id: 'app/root', input: 'wrappers' }, + configSchema: schema, + disabled: true, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + component: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + createExtensionTester( + createPageExtension({ + defaultPath: '/', + loader: async () =>
Hello
, + }), + ) + .add(extension, { config: { name: 'Robin' } }) + .add( + createExtension({ + attachTo: { id: 'app-wrapper-component:ns/test', input: 'children' }, + output: { element: coreExtensionData.reactElement }, + factory: () => ({ element:
}), + }), + ) + .render(); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + await expect(screen.findByTestId('Robin-1')).resolves.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx new file mode 100644 index 0000000000..1f09b873cc --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2023 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, { ComponentType, PropsWithChildren } from 'react'; +import { PortableSchema } from '../schema/types'; +import { + AnyExtensionInputMap, + ExtensionDefinition, + ResolvedExtensionInputs, + createExtension, +} from '../wiring/createExtension'; +import { createExtensionDataRef } from '../wiring/createExtensionDataRef'; +import { Expand } from '../types'; + +/** + * Creates an extension that renders a React wrapper at the app root, enclosing + * the app layout. This is useful for example for adding global React contexts + * and similar. + * + * @public + */ +export function createAppRootWrapperExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + Component: ComponentType< + PropsWithChildren<{ + inputs: Expand>; + config: TConfig; + }> + >; +}): ExtensionDefinition { + return createExtension({ + kind: 'app-wrapper-component', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/root', input: 'wrappers' }, + configSchema: options.configSchema, + disabled: options.disabled, + inputs: options.inputs, + output: { + component: createAppRootWrapperExtension.componentDataRef, + }, + factory({ inputs, config }) { + const Component = (props: PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + return { + component: Component, + }; + }, + }); +} + +/** @public */ +export namespace createAppRootWrapperExtension { + export const componentDataRef = + createExtensionDataRef>>( + 'app.root.wrapper', + ); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index fd2e38de49..40e9f6f56d 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -15,6 +15,8 @@ */ export { createApiExtension } from './createApiExtension'; +export { createAppRootElementExtension } from './createAppRootElementExtension'; +export { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; export { createNavLogoExtension } from './createNavLogoExtension'; diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 1a4ebbac48..2984af9dbf 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -14,7 +14,13 @@ * limitations under the License. */ -import React, { ComponentType, ReactNode, useContext, useState } from 'react'; +import React, { + ComponentType, + Fragment, + ReactNode, + useContext, + useState, +} from 'react'; import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; @@ -25,6 +31,7 @@ import { RouteRef, configApiRef, coreExtensionData, + createAppRootWrapperExtension, createExtension, createExtensionInput, createExtensionOverrides, @@ -63,7 +70,7 @@ const NavItem = (props: { ); }; -const TestCoreNavExtension = createExtension({ +const TestAppNavExtension = createExtension({ namespace: 'app', name: 'nav', attachTo: { id: 'app/layout', input: 'nav' }, @@ -149,36 +156,52 @@ const AuthenticationProvider = (props: { return children; }; -const TestCoreRouterExtension = createExtension({ +const TestAppRootExtension = createExtension({ namespace: 'app', name: 'root', attachTo: { id: 'app', input: 'root' }, inputs: { signInPage: createExtensionInput( - { - component: createSignInPageExtension.componentDataRef, - }, + { component: createSignInPageExtension.componentDataRef }, { singleton: true, optional: true }, ), children: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, + { element: coreExtensionData.reactElement }, { singleton: true }, ), + elements: createExtensionInput( + { element: coreExtensionData.reactElement }, + { optional: true }, + ), + wrappers: createExtensionInput( + { component: createAppRootWrapperExtension.componentDataRef }, + { optional: true }, + ), }, output: { element: coreExtensionData.reactElement, }, factory({ inputs }) { const SignInPage = inputs.signInPage?.output.component; - const children = inputs.children.output.element; + + let content: React.ReactNode = ( + <> + {inputs.elements.map(el => ( + {el.output.element} + ))} + {inputs.children.output.element} + + ); + + for (const wrapper of inputs.wrappers) { + content = {content}; + } return { element: ( - {children} + {content} ), @@ -278,8 +301,8 @@ export class ExtensionTester { createExtensionOverrides({ extensions: [ ...this.#extensions.map(extension => extension.definition), - TestCoreNavExtension, - TestCoreRouterExtension, + TestAppNavExtension, + TestAppRootExtension, ], }), ], From 26d01106783c9d6b9ba93deb1716f7ad2051fdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 12 Jan 2024 21:58:38 +0100 Subject: [PATCH 28/42] review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../frontend-app-api/src/extensions/AppRoot.tsx | 14 ++++++-------- .../createAppRootWrapperExtension.test.tsx | 6 +++--- .../extensions/createAppRootWrapperExtension.tsx | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index b442533ee6..883074ca00 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -54,14 +54,12 @@ export const AppRoot = createExtension({ { element: coreExtensionData.reactElement }, { singleton: true }, ), - elements: createExtensionInput( - { element: coreExtensionData.reactElement }, - { optional: true }, - ), - wrappers: createExtensionInput( - { component: createAppRootWrapperExtension.componentDataRef }, - { optional: true }, - ), + elements: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + wrappers: createExtensionInput({ + component: createAppRootWrapperExtension.componentDataRef, + }), }, output: { element: coreExtensionData.reactElement, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx index a0625ec1ae..d21cfa7583 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx @@ -33,7 +33,7 @@ describe('createAppRootWrapperExtension', () => { expect(extension).toEqual({ $$type: '@backstage/ExtensionDefinition', version: 'v1', - kind: 'app-wrapper-component', + kind: 'app-root-wrapper', attachTo: { id: 'app/root', input: 'wrappers' }, disabled: false, inputs: {}, @@ -79,7 +79,7 @@ describe('createAppRootWrapperExtension', () => { expect(extension).toEqual({ $$type: '@backstage/ExtensionDefinition', version: 'v1', - kind: 'app-wrapper-component', + kind: 'app-root-wrapper', namespace: 'ns', name: 'test', attachTo: { id: 'app/root', input: 'wrappers' }, @@ -106,7 +106,7 @@ describe('createAppRootWrapperExtension', () => { .add(extension, { config: { name: 'Robin' } }) .add( createExtension({ - attachTo: { id: 'app-wrapper-component:ns/test', input: 'children' }, + attachTo: { id: 'app-root-wrapper:ns/test', input: 'children' }, output: { element: coreExtensionData.reactElement }, factory: () => ({ element:
}), }), diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index 1f09b873cc..bbc51806af 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -50,7 +50,7 @@ export function createAppRootWrapperExtension< >; }): ExtensionDefinition { return createExtension({ - kind: 'app-wrapper-component', + kind: 'app-root-wrapper', namespace: options.namespace, name: options.name, attachTo: options.attachTo ?? { id: 'app/root', input: 'wrappers' }, From b5f6a1dd00150ff2270464abd434ce6345be7469 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 12 Jan 2024 17:03:38 -0500 Subject: [PATCH 29/42] document catalog cluster locator method and arrange the cluster locator method subsections in alphabetical order. Signed-off-by: Jamie Klassen --- docs/features/kubernetes/configuration.md | 59 +++++++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 494e51320a..2b43d199b6 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -65,20 +65,63 @@ This is an array used to determine where to retrieve cluster configuration from. Valid cluster locator methods are: - [`catalog`](#catalog) -- [`localKubectlProxy`](#localkubectlproxy) - [`config`](#config) - [`gke`](#gke) +- [`localKubectlProxy`](#localkubectlproxy) - [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier) #### `catalog` -This cluster locator method will read cluster information from the catalog. +This cluster locator method will gather +[Resources](https://backstage.io/docs/features/software-catalog/system-model#resource) +of +[type](https://backstage.io/docs/features/software-catalog/descriptor-format#spectype-required-4) +`kubernetes-cluster` from the catalog and treat them as clusters for the +purposes of the Kubernetes plugin. In order for a resource to be detected by +this method, it must also have the following +[annotations](https://backstage.io/docs/features/software-catalog/descriptor-format#annotations-optional) +(as seen +[here](https://github.com/backstage/backstage/blob/86baccb2d7d378baed74eaebf017c60b410986e5/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts#L51-L61) +in the code): -#### `localKubectlProxy` +- [`kubernetes.io/api-server`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server/), + denoting the base URL of the Kubernetes control plane +- [`kubernetes.io/api-server-certificate-authority`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server_ca/), + containing a base64-encoded certificate authority bundle in PEM format; + Backstage will check that the control plane presents a certificate signed by + this authority. +- [`kubernetes.io/auth-provider`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_auth_provider/), + denoting the strategy to use to authenticate with the control plane. -This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001). +There are many other annotations that can be applied to a cluster resource to +configure the way Backstage communicates, documented +[here](https://backstage.io/docs/reference/plugin-kubernetes-common#variables) +in the API reference. Here is a YAML snippet illustrating an example of a +cluster in the catalog: -NOTE: This cluster locator method is for local development only and should not be used in production. +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: my-cluster + annotations: + kubernetes.io/api-server: 'https://127.0.0.1:53725' + kubernetes.io/api-server-certificate-authority: # base64-encoded CA + kubernetes.io/auth-provider: 'oidc' + kubernetes.io/oidc-token-provider: 'microsoft' + kubernetes.io/skip-metrics-lookup: 'true' +spec: + type: kubernetes-cluster + owner: user:guest +``` + +Note that it is insecure to store a Kubernetes service account token in an +annotation on a catalog entity (where it could easily be accidentally revealed +by the catalog API) -- therefore there is no annotation corresponding to the +[`serviceAccountToken` field](#clustersserviceaccounttoken-optional) used by +the [`config`](#config) cluster locator. Accordingly, the catalog cluster +locator does not support the [`serviceAccount`](#clustersauthprovider) auth +strategy. #### `config` @@ -388,6 +431,12 @@ Defaults to `false`. Array of key value labels used to filter out clusters which don't have the matching [resource labels](https://cloud.google.com/resource-manager/docs/creating-managing-labels). +#### `localKubectlProxy` + +This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001). + +NOTE: This cluster locator method is for local development only and should not be used in production. + #### Custom `KubernetesClustersSupplier` If the configuration-based cluster locators do not work for your use-case, From 4ef6f1bde6c33af863cd35fc97407e4dfecbbb6d Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 12 Jan 2024 18:09:14 -0500 Subject: [PATCH 30/42] mention ingestion procedure synergy Signed-off-by: Jamie Klassen --- docs/features/kubernetes/configuration.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 2b43d199b6..612d57334d 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -123,6 +123,15 @@ the [`config`](#config) cluster locator. Accordingly, the catalog cluster locator does not support the [`serviceAccount`](#clustersauthprovider) auth strategy. +This method can be quite helpful when used in combination with an ingestion +procedure like the +[`GkeEntityProvider`](https://backstage.io/docs/reference/plugin-catalog-backend-module-gcp.gkeentityprovider/) +(installation documented +[here](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-gcp#installation)) +or the +[`AwsEKSClusterProcessor`](https://backstage.io/docs/reference/plugin-catalog-backend-module-aws.awseksclusterprocessor/) +to automatically update the set of clusters tracked by Backstage. + #### `config` This cluster locator method will read cluster information from your app-config From e8ce05959e30c1e1028b15d429546741c65fa854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 12 Jan 2024 13:58:56 +0100 Subject: [PATCH 31/42] add router to the app root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/silent-horses-raise.md | 2 +- .../src/extensions/AppRoot.tsx | 33 +++- packages/frontend-plugin-api/api-report.md | 33 ++++ packages/frontend-plugin-api/package.json | 32 ++-- .../extensions/createRouterExtension.test.tsx | 167 ++++++++++++++++++ .../src/extensions/createRouterExtension.tsx | 84 +++++++++ .../src/extensions/index.ts | 1 + .../src/app/createExtensionTester.test.tsx | 12 +- .../src/app/createExtensionTester.tsx | 135 +------------- .../src/app/renderInTestApp.test.tsx | 12 +- 10 files changed, 352 insertions(+), 159 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx diff --git a/.changeset/silent-horses-raise.md b/.changeset/silent-horses-raise.md index de2671c3da..a03d662b03 100644 --- a/.changeset/silent-horses-raise.md +++ b/.changeset/silent-horses-raise.md @@ -4,4 +4,4 @@ '@backstage/frontend-app-api': patch --- -Added `elements` and `wrappers` inputs to `app/root`, that let you add things to the root of the React tree above the layout. You can use the `createAppRootElementExtension` and `createAppRootWrapperExtension` extension creator, respectively, to conveniently create such extensions. +Added `elements`, `wrappers`, and `router` inputs to `app/root`, that let you add things to the root of the React tree above the layout. You can use the `createAppRootElementExtension`, `createAppRootWrapperExtension`, and `createRouterExtension` extension creator, respectively, to conveniently create such extensions. These are all optional, and if you do not supply a router a default one will be used (`BrowserRouter` in regular runs, `MemoryRouter` in tests/CI). diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index 883074ca00..2b709f4e42 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -17,6 +17,7 @@ import React, { ComponentType, Fragment, + PropsWithChildren, ReactNode, useContext, useState, @@ -26,6 +27,7 @@ import { createAppRootWrapperExtension, createExtension, createExtensionInput, + createRouterExtension, createSignInPageExtension, } from '@backstage/frontend-plugin-api'; import { @@ -46,6 +48,10 @@ export const AppRoot = createExtension({ name: 'root', attachTo: { id: 'app', input: 'root' }, inputs: { + router: createExtensionInput( + { component: createRouterExtension.componentDataRef }, + { singleton: true, optional: true }, + ), signInPage: createExtensionInput( { component: createSignInPageExtension.componentDataRef }, { singleton: true, optional: true }, @@ -80,7 +86,10 @@ export const AppRoot = createExtension({ return { element: ( - + {content} ), @@ -133,12 +142,18 @@ function SignInPageWrapper({ export interface AppRouterProps { children?: ReactNode; SignInPageComponent?: ComponentType; + RouterComponent?: ComponentType>; +} + +function DefaultRouter(props: PropsWithChildren<{}>) { + const configApi = useApi(configApiRef); + const basePath = getBasePath(configApi); + return {props.children}; } /** * App router and sign-in page wrapper. * - * @public * @remarks * * The AppRouter provides the routing context and renders the sign-in page. @@ -147,7 +162,11 @@ export interface AppRouterProps { * the app, while providing routing and route tracking for the app. */ export function AppRouter(props: AppRouterProps) { - const { children, SignInPageComponent } = props; + const { + children, + SignInPageComponent, + RouterComponent = DefaultRouter, + } = props; const configApi = useApi(configApiRef); const basePath = getBasePath(configApi); @@ -183,15 +202,15 @@ export function AppRouter(props: AppRouterProps) { ); return ( - + {children} - + ); } return ( - + {children} - + ); } diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 6113c946ee..1d42d8bab8 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -687,6 +687,39 @@ export function createRouteRef< } >; +// @public +export function createRouterExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + Component: ComponentType< + PropsWithChildren<{ + inputs: Expand>; + config: TConfig; + }> + >; +}): ExtensionDefinition; + +// @public (undocumented) +export namespace createRouterExtension { + const // (undocumented) + componentDataRef: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + {} + >; +} + // @public (undocumented) export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 4722cd1bff..007d76764b 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -22,6 +22,21 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/types": "workspace:^", + "@backstage/version-bridge": "workspace:^", + "@material-ui/core": "^4.12.4", + "@types/react": "^16.13.1 || ^17.0.0", + "lodash": "^4.17.21", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.21.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/frontend-app-api": "workspace:^", @@ -33,20 +48,5 @@ }, "files": [ "dist" - ], - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-router-dom": "6.0.0-beta.0 || ^6.3.0" - }, - "dependencies": { - "@backstage/core-components": "workspace:^", - "@backstage/core-plugin-api": "workspace:^", - "@backstage/types": "workspace:^", - "@backstage/version-bridge": "workspace:^", - "@material-ui/core": "^4.12.4", - "@types/react": "^16.13.1 || ^17.0.0", - "lodash": "^4.17.21", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.21.4" - } + ] } diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx new file mode 100644 index 0000000000..7109e1a8aa --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx @@ -0,0 +1,167 @@ +/* + * Copyright 2023 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 { createSpecializedApp } from '@backstage/frontend-app-api'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MockConfigApi } from '@backstage/test-utils'; +import { MemoryRouter } from 'react-router-dom'; +import { createSchemaFromZod } from '../schema/createSchemaFromZod'; +import { coreExtensionData } from '../wiring/coreExtensionData'; +import { createExtension } from '../wiring/createExtension'; +import { createExtensionInput } from '../wiring/createExtensionInput'; +import { createExtensionOverrides } from '../wiring/createExtensionOverrides'; +import { createPageExtension } from './createPageExtension'; +import { createRouterExtension } from './createRouterExtension'; + +describe('createRouterExtension', () => { + it('works with simple options and no props', async () => { + const extension = createRouterExtension({ + namespace: 'test', + Component: ({ children }) => ( + +
{children}
+
+ ), + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-router-component', + namespace: 'test', + attachTo: { id: 'app/root', input: 'router' }, + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createPageExtension({ + namespace: 'test', + defaultPath: '/', + loader: async () =>
, + }), + ], + }), + ], + }); + + render(app.createRoot()); + + await expect( + screen.findByTestId('test-contents'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByTestId('test-router'), + ).resolves.toBeInTheDocument(); + }); + + it('works with complex options and props', async () => { + const schema = createSchemaFromZod(z => z.object({ name: z.string() })); + + const extension = createRouterExtension({ + namespace: 'test', + name: 'test', + configSchema: schema, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + Component: ({ inputs, config, children }) => ( + +
+ {children} +
+
+ ), + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-router-component', + namespace: 'test', + name: 'test', + attachTo: { id: 'app/root', input: 'router' }, + configSchema: schema, + disabled: false, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + component: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createExtension({ + namespace: 'test', + attachTo: { + id: 'app-router-component:test/test', + input: 'children', + }, + output: { element: coreExtensionData.reactElement }, // doesn't matter + factory: () => ({ element:
}), + }), + createPageExtension({ + namespace: 'test', + defaultPath: '/', + loader: async () =>
, + }), + ], + }), + ], + config: new MockConfigApi({ + app: { + extensions: [ + { + 'app-router-component:test/test': { config: { name: 'Robin' } }, + }, + ], + }, + }), + }); + + render(app.createRoot()); + + await expect( + screen.findByTestId('test-contents'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByTestId('test-router-Robin-1'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx new file mode 100644 index 0000000000..32d61f7e60 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2023 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, { ComponentType, PropsWithChildren } from 'react'; +import { PortableSchema } from '../schema/types'; +import { + AnyExtensionInputMap, + ExtensionDefinition, + ResolvedExtensionInputs, + createExtension, +} from '../wiring/createExtension'; +import { createExtensionDataRef } from '../wiring/createExtensionDataRef'; +import { Expand } from '../types'; + +/** + * Creates an extension that replaces the router implementation at the app root. + * This is useful to be able to for example replace the BrowserRouter with a + * MemoryRouter in tests, or to add additional props to a BrowserRouter. + * + * @public + */ +export function createRouterExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + Component: ComponentType< + PropsWithChildren<{ + inputs: Expand>; + config: TConfig; + }> + >; +}): ExtensionDefinition { + return createExtension({ + kind: 'app-router-component', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/root', input: 'router' }, + configSchema: options.configSchema, + disabled: options.disabled, + inputs: options.inputs, + output: { + component: createRouterExtension.componentDataRef, + }, + factory({ inputs, config }) { + const Component = (props: PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + return { + component: Component, + }; + }, + }); +} + +/** @public */ +export namespace createRouterExtension { + export const componentDataRef = + createExtensionDataRef>>( + 'app.router.wrapper', + ); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 40e9f6f56d..562cb728cb 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -17,6 +17,7 @@ export { createApiExtension } from './createApiExtension'; export { createAppRootElementExtension } from './createAppRootElementExtension'; export { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; +export { createRouterExtension } from './createRouterExtension'; export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; export { createNavLogoExtension } from './createNavLogoExtension'; diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 40baaa3ac9..d53344f7fd 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -219,10 +219,14 @@ describe('createExtensionTester', () => { fireEvent.click(await screen.findByRole('button', { name: 'See details' })); await waitFor(() => - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'See details', - }), + expect(analyticsApiMock.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + action: 'click', + subject: 'See details', + }), + ]), + ), ); }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 2984af9dbf..a98879fbba 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -14,29 +14,20 @@ * limitations under the License. */ -import React, { - ComponentType, - Fragment, - ReactNode, - useContext, - useState, -} from 'react'; +import React from 'react'; import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { ExtensionDefinition, IconComponent, - IdentityApi, RouteRef, - configApiRef, coreExtensionData, - createAppRootWrapperExtension, createExtension, createExtensionInput, createExtensionOverrides, createNavItemExtension, - useApi, + createRouterExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; @@ -44,15 +35,7 @@ import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { createSignInPageExtension } from '../../../frontend-plugin-api/src/extensions/createSignInPageExtension'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/createExtension'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { InternalAppContext } from '../../../frontend-app-api/src/wiring/InternalAppContext'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { SignInPageProps } from '../../../core-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { getBasePath } from '../../../core-app-api/src/app/AppRouter'; const NavItem = (props: { routeRef: RouteRef; @@ -102,113 +85,6 @@ const TestAppNavExtension = createExtension({ }, }); -const AuthenticationProvider = (props: { - signInPage?: ComponentType; - children: ReactNode; -}) => { - const { signInPage: SignInPage, children } = props; - const configApi = useApi(configApiRef); - const signOutTargetUrl = getBasePath(configApi) || '/'; - - const internalAppContext = useContext(InternalAppContext); - if (!internalAppContext) { - throw new Error('AppRouter must be rendered within the AppProvider'); - } - - const { appIdentityProxy } = internalAppContext; - const [identityApi, setIdentityApi] = useState(); - - if (!SignInPage) { - appIdentityProxy.setTarget( - { - getUserId: () => 'guest', - getIdToken: async () => undefined, - getProfile: () => ({ - email: 'guest@example.com', - displayName: 'Guest', - }), - getProfileInfo: async () => ({ - email: 'guest@example.com', - displayName: 'Guest', - }), - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - getCredentials: async () => ({}), - signOut: async () => {}, - }, - { signOutTargetUrl }, - ); - - return children; - } - - if (!identityApi) { - return ; - } - - appIdentityProxy.setTarget(identityApi, { - signOutTargetUrl, - }); - - return children; -}; - -const TestAppRootExtension = createExtension({ - namespace: 'app', - name: 'root', - attachTo: { id: 'app', input: 'root' }, - inputs: { - signInPage: createExtensionInput( - { component: createSignInPageExtension.componentDataRef }, - { singleton: true, optional: true }, - ), - children: createExtensionInput( - { element: coreExtensionData.reactElement }, - { singleton: true }, - ), - elements: createExtensionInput( - { element: coreExtensionData.reactElement }, - { optional: true }, - ), - wrappers: createExtensionInput( - { component: createAppRootWrapperExtension.componentDataRef }, - { optional: true }, - ), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - const SignInPage = inputs.signInPage?.output.component; - - let content: React.ReactNode = ( - <> - {inputs.elements.map(el => ( - {el.output.element} - ))} - {inputs.children.output.element} - - ); - - for (const wrapper of inputs.wrappers) { - content = {content}; - } - - return { - element: ( - - - {content} - - - ), - }; - }, -}); - /** @public */ export class ExtensionTester { /** @internal */ @@ -302,7 +178,12 @@ export class ExtensionTester { extensions: [ ...this.#extensions.map(extension => extension.definition), TestAppNavExtension, - TestAppRootExtension, + createRouterExtension({ + namespace: 'test', + Component: ({ children }) => ( + {children} + ), + }), ], }), ], diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx index a6783d43a3..a56ae2f140 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx @@ -56,9 +56,13 @@ describe('renderInTestApp', () => { fireEvent.click(screen.getByRole('link', { name: 'See details' })); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'See details', - }); + expect(analyticsApiMock.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + action: 'click', + subject: 'See details', + }), + ]), + ); }); }); From 398b44907c2487393fc106b45a8f9c754d84216a Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 14 Jan 2024 14:51:17 +0100 Subject: [PATCH 32/42] Updated Template.v1beta3.schema.json, added a missing "presentation" field Signed-off-by: Bogdan Nechyporenko Signed-off-by: bnechyporenko --- .../src/Template.v1beta3.schema.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index d4be9b7538..d7698511d8 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -139,6 +139,24 @@ } ] }, + "presentation": { + "type": "object", + "description": "A way to redefine the labels for actionable buttons.", + "properties": { + "backButtonText": { + "type": "string", + "description": "A button which return the user to one step back." + }, + "createButtonText": { + "type": "string", + "description": "A button which start the execution of the template." + }, + "reviewButtonText": { + "type": "string", + "description": "A button which open the review step to verify the input prior to start the execution." + } + } + }, "steps": { "type": "array", "description": "A list of steps to execute.", From 178b8d8a70b7ba877236b8a9763877f71c534b7e Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 14 Jan 2024 14:51:50 +0100 Subject: [PATCH 33/42] Updated Template.v1beta3.schema.json, added a missing "presentation" field Signed-off-by: Bogdan Nechyporenko Signed-off-by: bnechyporenko --- .changeset/fresh-mirrors-shake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-mirrors-shake.md diff --git a/.changeset/fresh-mirrors-shake.md b/.changeset/fresh-mirrors-shake.md new file mode 100644 index 0000000000..a722182986 --- /dev/null +++ b/.changeset/fresh-mirrors-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-common': patch +--- + +Updated Template.v1beta3.schema.json, added a missing "presentation" field From 331389fccdfa9d96410cf67c1cf5e3c57b2052d7 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 14 Jan 2024 14:54:50 +0100 Subject: [PATCH 34/42] wip Signed-off-by: Bogdan Nechyporenko Signed-off-by: bnechyporenko --- .../src/Template.v1beta3.schema.json | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index d7698511d8..d4be9b7538 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -139,24 +139,6 @@ } ] }, - "presentation": { - "type": "object", - "description": "A way to redefine the labels for actionable buttons.", - "properties": { - "backButtonText": { - "type": "string", - "description": "A button which return the user to one step back." - }, - "createButtonText": { - "type": "string", - "description": "A button which start the execution of the template." - }, - "reviewButtonText": { - "type": "string", - "description": "A button which open the review step to verify the input prior to start the execution." - } - } - }, "steps": { "type": "array", "description": "A list of steps to execute.", From 04a57a034db2d90fe49d2c889222617f4190e91e Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 14 Jan 2024 14:57:15 +0100 Subject: [PATCH 35/42] wip Signed-off-by: Bogdan Nechyporenko Signed-off-by: bnechyporenko --- .../src/Template.v1beta3.schema.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index d4be9b7538..d7698511d8 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -139,6 +139,24 @@ } ] }, + "presentation": { + "type": "object", + "description": "A way to redefine the labels for actionable buttons.", + "properties": { + "backButtonText": { + "type": "string", + "description": "A button which return the user to one step back." + }, + "createButtonText": { + "type": "string", + "description": "A button which start the execution of the template." + }, + "reviewButtonText": { + "type": "string", + "description": "A button which open the review step to verify the input prior to start the execution." + } + } + }, "steps": { "type": "array", "description": "A list of steps to execute.", From 7b8e551a830cf57f36720c8f4701a46e9505ace2 Mon Sep 17 00:00:00 2001 From: lshwayne96 Date: Mon, 15 Jan 2024 12:37:57 +0800 Subject: [PATCH 36/42] Fix errors when deleting SQS messages Signed-off-by: lshwayne96 --- .changeset/shaggy-coins-happen.md | 8 ++++++++ .../src/publisher/AwsSqsConsumingEventPublisher.ts | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 .changeset/shaggy-coins-happen.md diff --git a/.changeset/shaggy-coins-happen.md b/.changeset/shaggy-coins-happen.md new file mode 100644 index 0000000000..c20a8e0cb2 --- /dev/null +++ b/.changeset/shaggy-coins-happen.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': patch +--- + +Fix errors when deleting SQS messages: + +- If zero messages were received, skip deletion to avoid `EmptyBatchRequest` error from the SQS client. +- If zero failures were returned from the SQS client during deletion, skip error logging. diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index 4f4e6ea55d..a2f6a00fe7 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -111,7 +111,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { } private async deleteMessages(messages?: Message[]): Promise { - if (!messages) { + if (!messages || messages.length === 0) { return; } @@ -129,7 +129,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { const result = await this.sqs.send( new DeleteMessageBatchCommand(deleteParams), ); - if (result.Failed) { + if (result.Failed && result.Failed.length > 0) { this.logger.error( `Failed to delete ${result.Failed!.length} of ${ messages.length From 7689c72085b82d45fa40d1c342730fa3c940c43b Mon Sep 17 00:00:00 2001 From: Stefan Buck Date: Mon, 15 Jan 2024 13:39:06 +0100 Subject: [PATCH 37/42] update snyk logo Signed-off-by: Stefan Buck --- microsite/data/plugins/snyk-security.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/snyk-security.yaml b/microsite/data/plugins/snyk-security.yaml index 4abd068247..8c22d44510 100644 --- a/microsite/data/plugins/snyk-security.yaml +++ b/microsite/data/plugins/snyk-security.yaml @@ -5,6 +5,6 @@ authorUrl: https://snyk.io category: Security description: View Snyk scanned vulnerabilities and license compliance of your components directly in Backstage. documentation: https://github.com/snyk-tech-services/backstage-plugin-snyk/blob/main/README.md -iconUrl: https://storage.googleapis.com/snyk-technical-services.appspot.com/snyk-logo-vertical-black.png +iconUrl: https://github.com/snyk-tech-services/backstage-plugin-snyk/assets/109112986/95eb1b06-b3e8-4910-9233-11926e02fa18 npmPackageName: 'backstage-plugin-snyk' addedDate: '2021-01-22' From d16f85f237d54d5e501131bab7c0336b1ee3a0cc Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Mon, 15 Jan 2024 14:40:59 +0200 Subject: [PATCH 38/42] feat: show first scaffolder output text by default usually there's only one output text that could be shown automatically when the output is shown. Signed-off-by: Heikki Hellgren --- .changeset/sixty-plums-switch.md | 5 +++++ .../DefaultTemplateOutputs.test.tsx | 10 +++++++++- .../TemplateOutputs/DefaultTemplateOutputs.tsx | 17 +++++++++-------- .../components/TemplateOutputs/TextOutputs.tsx | 6 +++++- 4 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 .changeset/sixty-plums-switch.md diff --git a/.changeset/sixty-plums-switch.md b/.changeset/sixty-plums-switch.md new file mode 100644 index 0000000000..8da07609c6 --- /dev/null +++ b/.changeset/sixty-plums-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Show first scaffolder output text by default diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx index 8366dcadf8..4c631b86b0 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -25,7 +25,10 @@ describe('', () => { it('should render template output', async () => { const output = { links: [{ title: 'Link 1', url: 'https://backstage.io/' }], - text: [{ title: 'Text 1', content: 'Hello, **world**!' }], + text: [ + { title: 'Text 1', content: 'Hello, **world**!' }, + { title: 'Text 2', content: 'Hello, **mars**!' }, + ], }; const { getByRole } = await renderInTestApp( @@ -37,6 +40,11 @@ describe('', () => { }, ); + // first text output default visible + expect(getByRole('heading', { level: 2 }).innerHTML).toBe( + output.text[0].title, + ); + // test link outputs for (const link of output.links ?? []) { expect( diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index a495e4ca9e..c105c5a571 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -28,17 +28,18 @@ import { TextOutputs } from './TextOutputs'; export const DefaultTemplateOutputs = (props: { output?: ScaffolderTaskOutput; }) => { - const [textOutputIndex, setTextOutputIndex] = useState(); + const { output } = props; + const [textOutputIndex, setTextOutputIndex] = useState( + output?.text?.length ? 0 : undefined, + ); const textOutput = useMemo( () => - textOutputIndex !== undefined - ? props.output?.text?.[textOutputIndex] - : null, - [props.output, textOutputIndex], + textOutputIndex !== undefined ? output?.text?.[textOutputIndex] : null, + [output, textOutputIndex], ); - if (!props.output) { + if (!output) { return null; } @@ -48,11 +49,11 @@ export const DefaultTemplateOutputs = (props: { - + diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx index 7e64603e92..8f61aa6a30 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -47,7 +47,11 @@ export const TextOutputs = (props: { startIcon={} component="div" color="primary" - onClick={() => setIndex?.(index !== i ? i : undefined)} + onClick={() => { + if (index !== i) { + setIndex?.(i); + } + }} variant={index === i ? 'outlined' : undefined} > {title} From 9373b4c88ef97306d7f699d21428e5b7f0942f8f Mon Sep 17 00:00:00 2001 From: Daniel Laird Date: Mon, 15 Jan 2024 14:21:23 +0000 Subject: [PATCH 39/42] Resolve PR feedback Signed-off-by: Daniel Laird --- .changeset/fair-spies-rescue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fair-spies-rescue.md b/.changeset/fair-spies-rescue.md index 2b49d4b969..d7f1ea5275 100644 --- a/.changeset/fair-spies-rescue.md +++ b/.changeset/fair-spies-rescue.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend-module-github': minor +'@backstage/plugin-scaffolder-backend-module-github': patch --- Ensure `teamReviewers` list is unique before calling API From f5da6523c03fbfc8f6de6e72cfbc47de0a4386da Mon Sep 17 00:00:00 2001 From: armandocomellas1 Date: Mon, 15 Jan 2024 18:01:06 -0600 Subject: [PATCH 40/42] Change the description header of the changeset and also add a unit test for new URL method Signed-off-by: armandocomellas1 --- .changeset/thirty-cats-help.md | 2 +- .../modules/core/UrlReaderProcessor.test.ts | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.changeset/thirty-cats-help.md b/.changeset/thirty-cats-help.md index bb6011ca9c..9c5d24180d 100644 --- a/.changeset/thirty-cats-help.md +++ b/.changeset/thirty-cats-help.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': patch --- -Change script in **UrlReaderProcessor.ts** Replacing the line code 127 with method (new URL) to handle URL Reader from GCS with wildcard \* +Parse the URL using a different method rather than `git-url-parse` to support wildcards for URLs which are not VCS providers diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index c9575ce5cd..0a3bec7570 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -213,4 +213,31 @@ describe('UrlReaderProcessor', () => { expect(reader.search).toHaveBeenCalledTimes(1); }); + + it('parser return valid URL with wildcard *', async () => { + const logger = getVoidLogger(); + + const reader: jest.Mocked = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn().mockImplementation(async () => []), + }; + + const processor = new UrlReaderProcessor({ reader, logger }); + + const emit = jest.fn(); + + await processor.readLocation( + { + type: 'url', + target: 'https://storage.cloud.google.com/ah-backstage-poc-catalog/*', + }, + false, + emit, + defaultEntityDataParser, + mockCache, + ); + + expect(reader.search).toHaveReturned(); + }); }); From cbf15616cd7d1e0c1adddb6b257afd8c105982e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Jan 2024 08:55:24 +0100 Subject: [PATCH 41/42] exit pre mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 8190034fe5..2ca69b284d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.90", From f6e4c2a9353fc6707e9334608dca5f5ac7a676e5 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 16 Jan 2024 10:24:31 +0100 Subject: [PATCH 42/42] chore: update the test to ensure that it's called with the correct arguments Signed-off-by: blam --- .../src/modules/core/UrlReaderProcessor.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index 0a3bec7570..e2489df4b4 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -238,6 +238,9 @@ describe('UrlReaderProcessor', () => { mockCache, ); - expect(reader.search).toHaveReturned(); + expect(reader.search).toHaveBeenCalledWith( + 'https://storage.cloud.google.com/ah-backstage-poc-catalog/*', + { etag: undefined }, + ); }); });