From 90646b4c028e9a998a992cd399fe11f7300bd3e5 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 11 Oct 2023 11:48:45 +0200 Subject: [PATCH 01/70] Added wrapper around fetching users from gitlab group to make it fail gracefully Signed-off-by: Jasper Boeijenga --- .../src/lib/client.ts | 12 +++----- .../src/lib/types.ts | 6 +++- .../GitlabOrgDiscoveryEntityProvider.ts | 29 ++++++++++--------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 722f079a21..c4cdd444cb 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import fetch from 'node-fetch'; import { getGitLabRequestOptions, GitLabIntegrationConfig, } from '@backstage/integration'; +import fetch from 'node-fetch'; import { Logger } from 'winston'; + import { - GitLabGroup, GitLabDescendantGroupsResponse, + GitLabGroup, GitLabGroupMembersResponse, GitLabUser, + PagedResponse, } from './types'; export type CommonListOptions = { @@ -44,11 +45,6 @@ interface UserListOptions extends CommonListOptions { exclude_internal?: boolean | undefined; } -export type PagedResponse = { - items: T[]; - nextPage?: number; -}; - export class GitLabClient { private readonly config: GitLabIntegrationConfig; private readonly logger: Logger; diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index 946317a11a..fa2eb73da6 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +export type PagedResponse = { + items: T[]; + nextPage?: number; +}; + export type GitlabGroupDescription = { id: number; web_url: string; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index 732f9f9fac..54373791ff 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -13,31 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + Entity, + GroupEntity, + UserEntity, +} from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { GitLabIntegration, ScmIntegrations } from '@backstage/integration'; import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; +import { merge } from 'lodash'; import * as uuid from 'uuid'; import { Logger } from 'winston'; + import { GitLabClient, GitlabProviderConfig, paginated, readGitlabConfigs, } from '../lib'; -import { GitLabGroup, GitLabUser } from '../lib/types'; -import { - ANNOTATION_LOCATION, - ANNOTATION_ORIGIN_LOCATION, - Entity, - UserEntity, - GroupEntity, -} from '@backstage/catalog-model'; -import { merge } from 'lodash'; +import { GitLabGroup, GitLabUser, PagedResponse } from '../lib/types'; type Result = { scanned: number; @@ -245,9 +245,12 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { groupRes.scanned++; groupRes.matches.push(group); - const groupUsers = await client.getGroupMembers(group.full_path, [ - 'DIRECT', - ]); + let groupUsers: PagedResponse = { items: [] }; + try { + groupUsers = await client.getGroupMembers(group.full_path, ['DIRECT']); + } catch { + logger.error(`Failed fetching user for group: ${group.full_path}`); + } for (const groupUser of groupUsers.items) { const user = idMappedUser[groupUser.id]; From d732f176101f3f097bca4ef2fe05342e7cef7091 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 11 Oct 2023 11:57:07 +0200 Subject: [PATCH 02/70] Added changeset Signed-off-by: Jasper Boeijenga --- .changeset/fresh-schools-thank.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-schools-thank.md diff --git a/.changeset/fresh-schools-thank.md b/.changeset/fresh-schools-thank.md new file mode 100644 index 0000000000..a5ab99587b --- /dev/null +++ b/.changeset/fresh-schools-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Added try catch arround fetching gitlab group users to prevent refresh from failing completely while only a select number of groups might not be able to load correctly. From ee87bef45634b3d2daa08edc5a762fc373d86e4a Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 11 Oct 2023 12:43:41 +0200 Subject: [PATCH 03/70] Correct spelling in changeset Signed-off-by: Jasper Boeijenga --- .changeset/fresh-schools-thank.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fresh-schools-thank.md b/.changeset/fresh-schools-thank.md index a5ab99587b..1e32f82051 100644 --- a/.changeset/fresh-schools-thank.md +++ b/.changeset/fresh-schools-thank.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-gitlab': patch --- -Added try catch arround fetching gitlab group users to prevent refresh from failing completely while only a select number of groups might not be able to load correctly. +Added try catch around fetching gitlab group users to prevent refresh from failing completely while only a select number of groups might not be able to load correctly. From 1150a8cdeca6d79335f72e13f24b39f50a1872ae Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 11 Oct 2023 13:00:35 +0200 Subject: [PATCH 04/70] Add group in the error message from the graphQL query Signed-off-by: Jasper Boeijenga --- plugins/catalog-backend-module-gitlab/src/lib/client.ts | 6 +++++- .../src/providers/GitlabOrgDiscoveryEntityProvider.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index c4cdd444cb..5447dec9d4 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -236,7 +236,11 @@ export class GitLabClient { }, ).then(r => r.json()); if (response.errors) { - throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`); + throw new Error( + `GraphQL errors: ${JSON.stringify( + response.errors, + )}, for group: ${groupPath}`, + ); } if (!response.data.group?.groupMembers?.nodes) { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index 54373791ff..6dbb0d162b 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -249,7 +249,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { try { groupUsers = await client.getGroupMembers(group.full_path, ['DIRECT']); } catch { - logger.error(`Failed fetching user for group: ${group.full_path}`); + logger.error(`Failed fetching users for group: ${group.full_path}`); } for (const groupUser of groupUsers.items) { From a40a23776804eb448b89ac57bfe44571563a8ff8 Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Wed, 11 Oct 2023 14:12:02 +0200 Subject: [PATCH 05/70] Applied PR suggestions Signed-off-by: Jasper Boeijenga --- plugins/catalog-backend-module-gitlab/src/lib/client.ts | 6 +----- .../src/providers/GitlabOrgDiscoveryEntityProvider.ts | 6 ++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 5447dec9d4..c4cdd444cb 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -236,11 +236,7 @@ export class GitLabClient { }, ).then(r => r.json()); if (response.errors) { - throw new Error( - `GraphQL errors: ${JSON.stringify( - response.errors, - )}, for group: ${groupPath}`, - ); + throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`); } if (!response.data.group?.groupMembers?.nodes) { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index 6dbb0d162b..affa8a41cf 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -248,8 +248,10 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { let groupUsers: PagedResponse = { items: [] }; try { groupUsers = await client.getGroupMembers(group.full_path, ['DIRECT']); - } catch { - logger.error(`Failed fetching users for group: ${group.full_path}`); + } catch (e) { + logger.error( + `Failed fetching users for group '${group.full_path}': ${e}`, + ); } for (const groupUser of groupUsers.items) { From ec779d4d8cc14aea51930ad0b372683465387607 Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Fri, 20 Oct 2023 12:16:40 +0100 Subject: [PATCH 06/70] added new backend system package to code-coverage-backend Signed-off-by: Josh Uvi --- plugins/code-coverage-backend/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 9ee98d52ad..d1fa4e4aed 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 0096b61d7e..6ba1771c8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6096,6 +6096,7 @@ __metadata: resolution: "@backstage/plugin-code-coverage-backend@workspace:plugins/code-coverage-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" From 11f671eaa94c06f9a73ffceeabefbf0cc1e0ff87 Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Fri, 20 Oct 2023 12:22:09 +0100 Subject: [PATCH 07/70] Added support for new backend system Signed-off-by: Josh Uvi --- .changeset/large-forks-arrive.md | 5 ++ plugins/code-coverage-backend/README.md | 23 ++++++++ plugins/code-coverage-backend/api-report.md | 5 ++ plugins/code-coverage-backend/src/index.ts | 1 + plugins/code-coverage-backend/src/plugin.ts | 60 +++++++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 .changeset/large-forks-arrive.md create mode 100644 plugins/code-coverage-backend/src/plugin.ts diff --git a/.changeset/large-forks-arrive.md b/.changeset/large-forks-arrive.md new file mode 100644 index 0000000000..3490a87128 --- /dev/null +++ b/.changeset/large-forks-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Added support for new backend system diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 54cf201015..4d5c473c56 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -67,6 +67,29 @@ diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts apiRouter.use(notFoundHandler()); ``` +## New Backend System + +The code coverage backend plugin has support for the [new backend system](https://backstage.io/docs/backend-system/), here's how you can set that up: + +In your `packages/backend/src/index.ts` make the following changes: + +```diff + import { createBackend } from '@backstage/backend-defaults'; ++ import { codeCoveragePlugin } from '@backstage/plugin-code-coverage-backend'; + const backend = createBackend(); + // ... other feature additions ++ backend.add(codeCoveragePlugin()); + backend.start(); +``` + +Alternatively, you can actually remove the import line above, and do this instead. + +```diff + backend.add(explorePlugin()); ++ backend.add(import('@backstage/plugin-explore-backend')); + +``` + ## Configuring your entity In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index 440af50714..8a64361391 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; @@ -11,6 +12,10 @@ import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; +// @public +const codeCoveragePlugin: () => BackendFeature; +export default codeCoveragePlugin; + // @public export function createRouter(options: RouterOptions): Promise; diff --git a/plugins/code-coverage-backend/src/index.ts b/plugins/code-coverage-backend/src/index.ts index 8511c9b71e..31de2ab3e3 100644 --- a/plugins/code-coverage-backend/src/index.ts +++ b/plugins/code-coverage-backend/src/index.ts @@ -22,3 +22,4 @@ export { createRouter } from './service/router'; export type { RouterOptions } from './service/router'; +export { codeCoveragePlugin as default } from './plugin'; diff --git a/plugins/code-coverage-backend/src/plugin.ts b/plugins/code-coverage-backend/src/plugin.ts new file mode 100644 index 0000000000..340280795f --- /dev/null +++ b/plugins/code-coverage-backend/src/plugin.ts @@ -0,0 +1,60 @@ +/* + * 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 { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * Code coverage backend plugin + * + * @public + */ +export const codeCoveragePlugin = createBackendPlugin({ + pluginId: 'codeCoverage', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + urlReader: coreServices.urlReader, + httpRouter: coreServices.httpRouter, + discovery: coreServices.discovery, + database: coreServices.database, + }, + async init({ + config, + logger, + urlReader, + httpRouter, + discovery, + database, + }) { + httpRouter.use( + await createRouter({ + config, + logger: loggerToWinstonLogger(logger), + urlReader, + discovery, + database, + }), + ); + }, + }); + }, +}); From 018007bdb8a395c9d5bfa0cd5becde27814fdc9c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 20 Oct 2023 16:52:25 +0200 Subject: [PATCH 08/70] bump backstage/actions to v0.6.5 Signed-off-by: Patrik Oldsberg --- .github/workflows/automate_changeset_feedback.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/cron.yml | 2 +- .github/workflows/deploy_docker-image.yml | 2 +- .github/workflows/deploy_nightly.yml | 2 +- .github/workflows/deploy_packages.yml | 4 ++-- .github/workflows/issue.yaml | 2 +- .github/workflows/pr-review-comment.yaml | 2 +- .github/workflows/pr.yaml | 2 +- .github/workflows/sync_code-formatting.yml | 2 +- .github/workflows/sync_snyk-github-issues.yml | 2 +- .github/workflows/uffizzi-build.yml | 2 +- .github/workflows/verify_accessibility.yml | 2 +- .github/workflows/verify_e2e-kubernetes.yml | 2 +- .github/workflows/verify_e2e-linux.yml | 2 +- .github/workflows/verify_e2e-windows.yml | 2 +- .github/workflows/verify_storybook.yml | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index 483d59dc8c..eac020cb8e 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -34,7 +34,7 @@ jobs: ref: 'refs/pull/${{ github.event.pull_request.number }}/merge' - name: fetch base run: git fetch --depth 1 origin ${{ github.base_ref }} - - uses: backstage/actions/changeset-feedback@v0.6.4 + - uses: backstage/actions/changeset-feedback@v0.6.5 name: Generate feedback with: diff-ref: 'origin/master' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81ea6f96d2..23e81192c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -76,7 +76,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -207,7 +207,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 2874be512c..79cfe9d790 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -13,7 +13,7 @@ jobs: with: egress-policy: audit - - uses: backstage/actions/cron@v0.6.4 + - uses: backstage/actions/cron@v0.6.5 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 30e7badcd0..69776da2d9 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -31,7 +31,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index d5fe9f5a64..7184557e12 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -27,7 +27,7 @@ jobs: node-version: 18.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@497722d45fb5a35c14c721f63ed74e6ad5f629dd # v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v18.x diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index f5ebfd7ee2..32f4b139f2 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -68,7 +68,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -150,7 +150,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index 1c1c028f20..f5150a3a01 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -15,4 +15,4 @@ jobs: egress-policy: audit - name: Issue sync - uses: backstage/actions/issue-sync@v0.6.4 + uses: backstage/actions/issue-sync@v0.6.5 diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index e557c85d51..8d66567ff4 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -40,7 +40,7 @@ jobs: const prNumber = artifact.name.slice('pr_number-'.length) core.setOutput('pr-number', prNumber); - - uses: backstage/actions/re-review@v0.6.4 + - uses: backstage/actions/re-review@v0.6.5 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8c99f105a9..89193c30ab 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -23,7 +23,7 @@ jobs: egress-policy: audit - name: PR sync - uses: backstage/actions/pr-sync@v0.6.4 + uses: backstage/actions/pr-sync@v0.6.5 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index fa8d91f25a..610ad323e2 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -25,7 +25,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 690e2fb76e..2be954746b 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -24,7 +24,7 @@ jobs: node-version: 18.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v18.x diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index 3107b87b8f..375c6bdcd2 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -34,7 +34,7 @@ jobs: registry-url: https://registry.npmjs.org/ - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: linux-v18 diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index d53f3834d2..f34f2f5506 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -30,7 +30,7 @@ jobs: with: node-version: 18.x - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v18.x - name: run Lighthouse CI diff --git a/.github/workflows/verify_e2e-kubernetes.yml b/.github/workflows/verify_e2e-kubernetes.yml index cddd6589d7..ea77609614 100644 --- a/.github/workflows/verify_e2e-kubernetes.yml +++ b/.github/workflows/verify_e2e-kubernetes.yml @@ -35,7 +35,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 744a5d7e74..6b61053628 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -57,7 +57,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 1385983c24..7ee5e596c5 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -78,7 +78,7 @@ jobs: uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # latest - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index f0354df296..e992a2d658 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -42,7 +42,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.6.4 + uses: backstage/actions/yarn-install@v0.6.5 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install From 57fda44b90b2936961a40719740ec12ab5b3b3aa Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Fri, 20 Oct 2023 13:03:10 -0400 Subject: [PATCH 09/70] Upgrade GraphiQL to 3.0.6 Signed-off-by: Taras Mankovski --- .changeset/four-files-behave.md | 5 + plugins/graphiql/package.json | 2 +- .../GraphiQLBrowser/GraphiQLBrowser.tsx | 7 +- yarn.lock | 940 +++++++++++++++++- 4 files changed, 926 insertions(+), 28 deletions(-) create mode 100644 .changeset/four-files-behave.md diff --git a/.changeset/four-files-behave.md b/.changeset/four-files-behave.md new file mode 100644 index 0000000000..3e218aa7e8 --- /dev/null +++ b/.changeset/four-files-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-graphiql': patch +--- + +Upgrade to GraphiQL 3.0.6 diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 52cff1fdb7..409b3e99f7 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -54,7 +54,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@types/react": "^16.13.1 || ^17.0.0", - "graphiql": "^1.5.12", + "graphiql": "^3.0.6", "graphql": "^16.0.0", "graphql-ws": "^5.4.1", "react-use": "^17.2.4" diff --git a/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx index dd38f513be..40c2c168a8 100644 --- a/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx +++ b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx @@ -77,12 +77,7 @@ export const GraphiQLBrowser = (props: GraphiQLBrowserProps) => {
- +
diff --git a/yarn.lock b/yarn.lock index 3075745d85..3b5c2c7866 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3305,12 +3305,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.1, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.23.1 - resolution: "@babel/runtime@npm:7.23.1" +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.1, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.23.2 + resolution: "@babel/runtime@npm:7.23.2" dependencies: regenerator-runtime: ^0.14.0 - checksum: 0cd0d43e6e7dc7f9152fda8c8312b08321cda2f56ef53d6c22ebdd773abdc6f5d0a69008de90aa41908d00e2c1facb24715ff121274e689305c858355ff02c70 + checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb languageName: node linkType: hard @@ -7117,7 +7117,7 @@ __metadata: "@types/codemirror": ^5.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - graphiql: ^1.5.12 + graphiql: ^3.0.6 graphql: ^16.0.0 graphql-ws: ^5.4.1 msw: ^1.0.0 @@ -10520,6 +10520,15 @@ __metadata: languageName: node linkType: hard +"@emotion/is-prop-valid@npm:^0.8.2": + version: 0.8.8 + resolution: "@emotion/is-prop-valid@npm:0.8.8" + dependencies: + "@emotion/memoize": 0.7.4 + checksum: bb7ec6d48c572c540e24e47cc94fc2f8dec2d6a342ae97bc9c8b6388d9b8d283862672172a1bb62d335c02662afe6291e10c71e9b8642664a8b43416cdceffac + languageName: node + linkType: hard + "@emotion/is-prop-valid@npm:^1.1.0, @emotion/is-prop-valid@npm:^1.2.1": version: 1.2.1 resolution: "@emotion/is-prop-valid@npm:1.2.1" @@ -10529,6 +10538,13 @@ __metadata: languageName: node linkType: hard +"@emotion/memoize@npm:0.7.4": + version: 0.7.4 + resolution: "@emotion/memoize@npm:0.7.4" + checksum: 4e3920d4ec95995657a37beb43d3f4b7d89fed6caa2b173a4c04d10482d089d5c3ea50bbc96618d918b020f26ed6e9c4026bbd45433566576c1f7b056c3271dc + languageName: node + linkType: hard + "@emotion/memoize@npm:^0.8.1": version: 0.8.1 resolution: "@emotion/memoize@npm:0.8.1" @@ -11251,7 +11267,7 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.0.2": +"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.0.2": version: 2.0.2 resolution: "@floating-ui/react-dom@npm:2.0.2" dependencies: @@ -11427,6 +11443,33 @@ __metadata: languageName: node linkType: hard +"@graphiql/react@npm:^0.19.4": + version: 0.19.4 + resolution: "@graphiql/react@npm:0.19.4" + dependencies: + "@graphiql/toolkit": ^0.9.1 + "@headlessui/react": ^1.7.15 + "@radix-ui/react-dialog": ^1.0.4 + "@radix-ui/react-dropdown-menu": ^2.0.5 + "@radix-ui/react-tooltip": ^1.0.6 + "@radix-ui/react-visually-hidden": ^1.0.3 + "@types/codemirror": ^5.60.8 + clsx: ^1.2.1 + codemirror: ^5.65.3 + codemirror-graphql: ^2.0.10 + copy-to-clipboard: ^3.2.0 + framer-motion: ^6.5.1 + graphql-language-service: ^5.2.0 + markdown-it: ^12.2.0 + set-value: ^4.1.0 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: 08eb53fc0449d5814ba92faff8e03ba77e97ecd4d3d473d115d1219e52b59d9a1cc1b0f34c41315b3a0185fb862646c1affd2094e1f5f524328a95c8e4e8b591 + languageName: node + linkType: hard + "@graphiql/toolkit@npm:^0.6.1": version: 0.6.1 resolution: "@graphiql/toolkit@npm:0.6.1" @@ -11440,6 +11483,22 @@ __metadata: languageName: node linkType: hard +"@graphiql/toolkit@npm:^0.9.1": + version: 0.9.1 + resolution: "@graphiql/toolkit@npm:0.9.1" + dependencies: + "@n1ru4l/push-pull-async-iterable-iterator": ^3.1.0 + meros: ^1.1.4 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + graphql-ws: ">= 4.5.0" + peerDependenciesMeta: + graphql-ws: + optional: true + checksum: 5328426051b7f9a9ffbd569c950d1a103ce0e2ee7b5d7a57f3d899488ad43d1a5101e8aeced7416e106c7687d67bb7981aa7e87dea5b0f17b77569aa738bf3b5 + languageName: node + linkType: hard + "@graphql-codegen/cli@npm:^3.0.0": version: 3.3.1 resolution: "@graphql-codegen/cli@npm:3.3.1" @@ -12222,6 +12281,18 @@ __metadata: languageName: node linkType: hard +"@headlessui/react@npm:^1.7.15": + version: 1.7.17 + resolution: "@headlessui/react@npm:1.7.17" + dependencies: + client-only: ^0.0.1 + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + checksum: 0cdb67747e7f606f78214dac0b48573247779e70534b4471515c094b74addda173dc6a9847d33aea9c6e6bc151016c034125328953077e32aa7947ebabed91f7 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.11": version: 0.11.11 resolution: "@humanwhocodes/config-array@npm:0.11.11" @@ -13358,6 +13429,71 @@ __metadata: languageName: node linkType: hard +"@motionone/animation@npm:^10.12.0": + version: 10.16.3 + resolution: "@motionone/animation@npm:10.16.3" + dependencies: + "@motionone/easing": ^10.16.3 + "@motionone/types": ^10.16.3 + "@motionone/utils": ^10.16.3 + tslib: ^2.3.1 + checksum: 797cacea335e6f892af27579eff51450dcf18c5bbc5c0ca44a000929b21857f4afb974ffb411c4935bfbd01ef2ddb3ef542ba3313ae66e1e5392b5d314df6ad3 + languageName: node + linkType: hard + +"@motionone/dom@npm:10.12.0": + version: 10.12.0 + resolution: "@motionone/dom@npm:10.12.0" + dependencies: + "@motionone/animation": ^10.12.0 + "@motionone/generators": ^10.12.0 + "@motionone/types": ^10.12.0 + "@motionone/utils": ^10.12.0 + hey-listen: ^1.0.8 + tslib: ^2.3.1 + checksum: 123356f28e44362c4f081aae3df22e576f46bfcb07e01257b2ac64a115668448f29b8de67e4b6e692c5407cffb78ffe7cf9fa1bc064007482bab5dd23a69d380 + languageName: node + linkType: hard + +"@motionone/easing@npm:^10.16.3": + version: 10.16.3 + resolution: "@motionone/easing@npm:10.16.3" + dependencies: + "@motionone/utils": ^10.16.3 + tslib: ^2.3.1 + checksum: 03e2460cdd35ee4967a86ce28ffbaaaca589263f659f652801cf6bd667baba9b3d5ce6d134df6b64413b60b34dd21d7c38b0cd8a4c3e1ed789789cdb971905b2 + languageName: node + linkType: hard + +"@motionone/generators@npm:^10.12.0": + version: 10.16.4 + resolution: "@motionone/generators@npm:10.16.4" + dependencies: + "@motionone/types": ^10.16.3 + "@motionone/utils": ^10.16.3 + tslib: ^2.3.1 + checksum: 185091c5cfbe67c38e84bf3920d1b5862e5d7eb624136494a7e4779b2f9d06855ebe3e633d95dcc5a1735d92d59d1ae28a0724c2f9d8bddd60fc9bc3603fab48 + languageName: node + linkType: hard + +"@motionone/types@npm:^10.12.0, @motionone/types@npm:^10.16.3": + version: 10.16.3 + resolution: "@motionone/types@npm:10.16.3" + checksum: ff38982f5aff2c0abbc3051c843d186d6f954c971e97dd6fced97a4ef50ee04f6e49607541ebb80e14dd143cf63553c388392110e270d04eca23f6b529f7f321 + languageName: node + linkType: hard + +"@motionone/utils@npm:^10.12.0, @motionone/utils@npm:^10.16.3": + version: 10.16.3 + resolution: "@motionone/utils@npm:10.16.3" + dependencies: + "@motionone/types": ^10.16.3 + hey-listen: ^1.0.8 + tslib: ^2.3.1 + checksum: d06025911c54c2217c98026cd38d4d681268a2b9b2830ac7342820881ba6be09721dd03626f52547749ead0543d5e2f2a69c9270ffdeaabc0949f7afb3233817 + languageName: node + linkType: hard + "@mswjs/cookies@npm:^0.2.2": version: 0.2.2 resolution: "@mswjs/cookies@npm:0.2.2" @@ -14598,6 +14734,564 @@ __metadata: languageName: node linkType: hard +"@radix-ui/primitive@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/primitive@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + checksum: 2b93e161d3fdabe9a64919def7fa3ceaecf2848341e9211520c401181c9eaebb8451c630b066fad2256e5c639c95edc41de0ba59c40eff37e799918d019822d1 + languageName: node + linkType: hard + +"@radix-ui/react-arrow@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-arrow@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 8cca086f0dbb33360e3c0142adf72f99fc96352d7086d6c2356dbb2ea5944cfb720a87d526fc48087741c602cd8162ca02b0af5e6fdf5f56d20fddb44db8b4c3 + languageName: node + linkType: hard + +"@radix-ui/react-collection@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-collection@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-slot": 1.0.2 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: acfbc9b0b2c553d343c22f02c9f098bc5cfa99e6e48df91c0d671855013f8b877ade9c657b7420a7aa523b5aceadea32a60dd72c23b1291f415684fb45d00cff + languageName: node + linkType: hard + +"@radix-ui/react-compose-refs@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-compose-refs@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 2b9a613b6db5bff8865588b6bf4065f73021b3d16c0a90b2d4c23deceeb63612f1f15de188227ebdc5f88222cab031be617a9dd025874c0487b303be3e5cc2a8 + languageName: node + linkType: hard + +"@radix-ui/react-context@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-context@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 60e9b81d364f40c91a6213ec953f7c64fcd9d75721205a494a5815b3e5ae0719193429b62ee6c7002cd6aaf70f8c0e2f08bdbaba9ffcc233044d32b56d2127d1 + languageName: node + linkType: hard + +"@radix-ui/react-dialog@npm:^1.0.4": + version: 1.0.5 + resolution: "@radix-ui/react-dialog@npm:1.0.5" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-focus-guards": 1.0.1 + "@radix-ui/react-focus-scope": 1.0.4 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-controllable-state": 1.0.1 + aria-hidden: ^1.1.1 + react-remove-scroll: 2.5.5 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 3d11ca31afb794a6dd286005ab7894cb0ce7bc2de5481de98900470b11d495256401306763de030f5e35aa545ff90d34632ffd54a1b29bf55afba813be4bb84a + languageName: node + linkType: hard + +"@radix-ui/react-direction@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-direction@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 5336a8b0d4f1cde585d5c2b4448af7b3d948bb63a1aadb37c77771b0e5902dc6266e409cf35fd0edaca7f33e26424be19e64fb8f9d7f7be2d6f1714ea2764210 + languageName: node + linkType: hard + +"@radix-ui/react-dismissable-layer@npm:1.0.5": + version: 1.0.5 + resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@radix-ui/react-use-escape-keydown": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: e73cf4bd3763f4d55b1bea7486a9700384d7d94dc00b1d5a75e222b2f1e4f32bc667a206ca4ed3baaaf7424dce7a239afd0ba59a6f0d89c3462c4e6e8d029a04 + languageName: node + linkType: hard + +"@radix-ui/react-dropdown-menu@npm:^2.0.5": + version: 2.0.6 + resolution: "@radix-ui/react-dropdown-menu@npm:2.0.6" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-menu": 2.0.6 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 1433e04234c29ae688b1d50b4a5ad0fd67e2627a5ea2e5f60fec6e4307e673ef35a703672eae0d61d96156c59084bbb19de9f9b9936b3fc351917dfe41dcf403 + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-focus-guards@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-focus-scope@npm:1.0.4" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 3481db1a641513a572734f0bcb0e47fefeba7bccd6ec8dde19f520719c783ef0b05a55ef0d5292078ed051cc5eda46b698d5d768da02e26e836022f46b376fd1 + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-id@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-layout-effect": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 446a453d799cc790dd2a1583ff8328da88271bff64530b5a17c102fa7fb35eece3cf8985359d416f65e330cd81aa7b8fe984ea125fc4f4eaf4b3801d698e49fe + languageName: node + linkType: hard + +"@radix-ui/react-menu@npm:2.0.6": + version: 2.0.6 + resolution: "@radix-ui/react-menu@npm:2.0.6" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-collection": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-direction": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-focus-guards": 1.0.1 + "@radix-ui/react-focus-scope": 1.0.4 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-roving-focus": 1.0.4 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-callback-ref": 1.0.1 + aria-hidden: ^1.1.1 + react-remove-scroll: 2.5.5 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: a43fb560dbb5a4ddc43ea4e2434a9f517bbbcbf8b12e1e74c1e36666ad321aef7e39f91770140c106fe6f34e237102be8a02f3bc5588e6c06a709e20580c5e82 + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-popper@npm:1.1.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@floating-ui/react-dom": ^2.0.0 + "@radix-ui/react-arrow": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@radix-ui/react-use-layout-effect": 1.0.1 + "@radix-ui/react-use-rect": 1.0.1 + "@radix-ui/react-use-size": 1.0.1 + "@radix-ui/rect": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: b18a15958623f9222b6ed3e24b9fbcc2ba67b8df5a5272412f261de1592b3f05002af1c8b94c065830c3c74267ce00cf6c1d70d4d507ec92ba639501f98aa348 + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-portal@npm:1.0.4" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: c4cf35e2f26a89703189d0eef3ceeeb706ae0832e98e558730a5e929ca7c72c7cb510413a24eca94c7732f8d659a1e81942bec7b90540cb73ce9e4885d040b64 + languageName: node + linkType: hard + +"@radix-ui/react-presence@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-presence@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-use-layout-effect": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: ed2ff9faf9e4257a4065034d3771459e5a91c2d840b2fcec94661761704dbcb65bcdd927d28177a2a129b3dab5664eb90a9b88309afe0257a9f8ba99338c0d95 + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-primitive@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-slot": 1.0.2 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 9402bc22923c8e5c479051974a721c301535c36521c0237b83e5fa213d013174e77f3ad7905e6d60ef07e14f88ec7f4ea69891dc7a2b39047f8d3640e8f8d713 + languageName: node + linkType: hard + +"@radix-ui/react-roving-focus@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-roving-focus@npm:1.0.4" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-collection": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-direction": 1.0.1 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@radix-ui/react-use-controllable-state": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 69b1c82c2d9db3ba71549a848f2704200dab1b2cd22d050c1e081a78b9a567dbfdc7fd0403ee010c19b79652de69924d8ca2076cd031d6552901e4213493ffc7 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-slot@npm:1.0.2" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: edf5edf435ff594bea7e198bf16d46caf81b6fb559493acad4fa8c308218896136acb16f9b7238c788fd13e94a904f2fd0b6d834e530e4cae94522cdb8f77ce9 + languageName: node + linkType: hard + +"@radix-ui/react-tooltip@npm:^1.0.6": + version: 1.0.7 + resolution: "@radix-ui/react-tooltip@npm:1.0.7" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-controllable-state": 1.0.1 + "@radix-ui/react-visually-hidden": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 894d448c69a3e4d7626759f9f6c7997018fe8ef9cde098393bd83e10743d493dfd284eef041e46accc45486d5a5cd5f76d97f56afbdace7aed6e0cb14007bf15 + languageName: node + linkType: hard + +"@radix-ui/react-use-callback-ref@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: b9fd39911c3644bbda14a84e4fca080682bef84212b8d8931fcaa2d2814465de242c4cfd8d7afb3020646bead9c5e539d478cea0a7031bee8a8a3bb164f3bc4c + languageName: node + linkType: hard + +"@radix-ui/react-use-controllable-state@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-callback-ref": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: dee2be1937d293c3a492cb6d279fc11495a8f19dc595cdbfe24b434e917302f9ac91db24e8cc5af9a065f3f209c3423115b5442e65a5be9fd1e9091338972be9 + languageName: node + linkType: hard + +"@radix-ui/react-use-escape-keydown@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-callback-ref": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: c6ed0d9ce780f67f924980eb305af1f6cce2a8acbaf043a58abe0aa3cc551d9aa76ccee14531df89bbee302ead7ecc7fce330886f82d4672c5eda52f357ef9b8 + languageName: node + linkType: hard + +"@radix-ui/react-use-layout-effect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: bed9c7e8de243a5ec3b93bb6a5860950b0dba359b6680c84d57c7a655e123dec9b5891c5dfe81ab970652e7779fe2ad102a23177c7896dde95f7340817d47ae5 + languageName: node + linkType: hard + +"@radix-ui/react-use-rect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-rect@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/rect": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 433f07e61e04eb222349825bb05f3591fca131313a1d03709565d6226d8660bd1d0423635553f95ee4fcc25c8f2050972d848808d753c388e2a9ae191ebf17f3 + languageName: node + linkType: hard + +"@radix-ui/react-use-size@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-size@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-layout-effect": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6cc150ad1e9fa85019c225c5a5d50a0af6cdc4653dad0c21b4b40cd2121f36ee076db326c43e6bc91a69766ccff5a84e917d27970176b592577deea3c85a3e26 + languageName: node + linkType: hard + +"@radix-ui/react-visually-hidden@npm:1.0.3, @radix-ui/react-visually-hidden@npm:^1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-visually-hidden@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 2e9d0c8253f97e7d6ffb2e52a5cfd40ba719f813b39c3e2e42c496d54408abd09ef66b5aec4af9b8ab0553215e32452a5d0934597a49c51dd90dc39181ed0d57 + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/rect@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + checksum: aeec13b234a946052512d05239067d2d63422f9ec70bf2fe7acfd6b9196693fc33fbaf43c2667c167f777d90a095c6604eb487e0bce79e230b6df0f6cacd6a55 + languageName: node + linkType: hard + "@react-hookz/deep-equal@npm:^1.0.4": version: 1.0.4 resolution: "@react-hookz/deep-equal@npm:1.0.4" @@ -17061,12 +17755,21 @@ __metadata: languageName: node linkType: hard -"@types/codemirror@npm:^5.0.0": - version: 5.60.10 - resolution: "@types/codemirror@npm:5.60.10" +"@types/codemirror@npm:^0.0.90": + version: 0.0.90 + resolution: "@types/codemirror@npm:0.0.90" dependencies: "@types/tern": "*" - checksum: c5977db03939f2a208f0ec7958be70b4fb205dd3f3122b2175ff28287a5424da95f9030b2838c61d37e6278ec53795861dec12439967c1e1da885b2b2a65b299 + checksum: f4594b9bc95306bbbe24d967e0749e28fe7b1e461c41621429b8c8bc295bda1704d99c1d7d5496efd987ee80d24f055155ddd742fa0c975cd69f279ccdaa0af9 + languageName: node + linkType: hard + +"@types/codemirror@npm:^5.0.0, @types/codemirror@npm:^5.60.8": + version: 5.60.12 + resolution: "@types/codemirror@npm:5.60.12" + dependencies: + "@types/tern": "*" + checksum: dff22f32ea42ccd3f9bfcf408631f94a11ffb4614ff4fa8cc55adf7da6e7ba96650533b8dd27d037242747bdaa85141e93520f84409db7bc394862a174a10e1e languageName: node linkType: hard @@ -19871,6 +20574,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.1.1": + version: 1.2.3 + resolution: "aria-hidden@npm:1.2.3" + dependencies: + tslib: ^2.0.0 + checksum: 7d7d211629eef315e94ed3b064c6823d13617e609d3f9afab1c2ed86399bb8e90405f9bdd358a85506802766f3ecb468af985c67c846045a34b973bcc0289db9 + languageName: node + linkType: hard + "aria-query@npm:5.1.3, aria-query@npm:^5.0.0, aria-query@npm:^5.1.3": version: 5.1.3 resolution: "aria-query@npm:5.1.3" @@ -21826,7 +22538,7 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.0.2, clsx@npm:^1.0.4, clsx@npm:^1.1.1": +"clsx@npm:^1.0.2, clsx@npm:^1.0.4, clsx@npm:^1.1.1, clsx@npm:^1.2.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 @@ -21890,6 +22602,20 @@ __metadata: languageName: node linkType: hard +"codemirror-graphql@npm:^2.0.10": + version: 2.0.10 + resolution: "codemirror-graphql@npm:2.0.10" + dependencies: + "@types/codemirror": ^0.0.90 + graphql-language-service: 5.2.0 + peerDependencies: + "@codemirror/language": 6.0.0 + codemirror: ^5.65.3 + graphql: ^15.5.0 || ^16.0.0 + checksum: d14e680168f407fd3d6ab09d88ea33640dd39f84d9ceb1ce605d9687d459fd08787109800a9847bbce72122e3318e975b255f65ddb19725d7a12781f4bf26506 + languageName: node + linkType: hard + "codemirror@npm:^5.65.3": version: 5.65.3 resolution: "codemirror@npm:5.65.3" @@ -23680,6 +24406,13 @@ __metadata: languageName: node linkType: hard +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449 + languageName: node + linkType: hard + "detect-node@npm:^2.0.4": version: 2.0.4 resolution: "detect-node@npm:2.0.4" @@ -26535,6 +27268,36 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^6.5.1": + version: 6.5.1 + resolution: "framer-motion@npm:6.5.1" + dependencies: + "@emotion/is-prop-valid": ^0.8.2 + "@motionone/dom": 10.12.0 + framesync: 6.0.1 + hey-listen: ^1.0.8 + popmotion: 11.0.3 + style-value-types: 5.0.0 + tslib: ^2.1.0 + peerDependencies: + react: ">=16.8 || ^17.0.0 || ^18.0.0" + react-dom: ">=16.8 || ^17.0.0 || ^18.0.0" + dependenciesMeta: + "@emotion/is-prop-valid": + optional: true + checksum: 737959063137b4ccafe01e0ac0c9e5a9531bf3f729f62c34ca7a5d7955e6664f70affd22b044f7db51df41acb21d120a4f71a860e17a80c4db766ad66f2153a1 + languageName: node + linkType: hard + +"framesync@npm:6.0.1": + version: 6.0.1 + resolution: "framesync@npm:6.0.1" + dependencies: + tslib: ^2.1.0 + checksum: a23ebe8f7e20a32c0b99c2f8175b6f07af3ec6316aad52a2316316a6d011d717af8d2175dcc2827031c59fabb30232ed3e19a720a373caba7f070e1eae436325 + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -26814,6 +27577,13 @@ __metadata: languageName: node linkType: hard +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: e2614e43b4694c78277bb61b0f04583d45786881289285c73770b07ded246a98be7e1f78b940c80cbe6f2b07f55f0b724e6db6fd6f1bcbd1e8bdac16521074ed + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -27198,7 +27968,7 @@ __metadata: languageName: node linkType: hard -"graphiql@npm:^1.5.12, graphiql@npm:^1.8.8": +"graphiql@npm:^1.8.8": version: 1.11.5 resolution: "graphiql@npm:1.11.5" dependencies: @@ -27215,6 +27985,22 @@ __metadata: languageName: node linkType: hard +"graphiql@npm:^3.0.6": + version: 3.0.6 + resolution: "graphiql@npm:3.0.6" + dependencies: + "@graphiql/react": ^0.19.4 + "@graphiql/toolkit": ^0.9.1 + graphql-language-service: ^5.2.0 + markdown-it: ^12.2.0 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: 34d35ca4b46ce1e0cef0362f15d091481d2275e4a5b537b5715cfbaee15b1e114c31f35a63558a6f74a754687fbf4bc977411818af34e2e471a96902370c40f2 + languageName: node + linkType: hard + "graphlib@npm:^2.1.8": version: 2.1.8 resolution: "graphlib@npm:2.1.8" @@ -27249,17 +28035,17 @@ __metadata: languageName: node linkType: hard -"graphql-language-service@npm:^5.0.6": - version: 5.0.6 - resolution: "graphql-language-service@npm:5.0.6" +"graphql-language-service@npm:5.2.0, graphql-language-service@npm:^5.0.6, graphql-language-service@npm:^5.2.0": + version: 5.2.0 + resolution: "graphql-language-service@npm:5.2.0" dependencies: nullthrows: ^1.0.0 - vscode-languageserver-types: ^3.15.1 + vscode-languageserver-types: ^3.17.1 peerDependencies: graphql: ^15.5.0 || ^16.0.0 bin: graphql: dist/temp-bin.js - checksum: a7155ba934aa428278cce0f460fa3b8b12020a26a0355e60738974617a66d9b2f1bb7d41cbd72a1620a29e61f10ca438cbf72cbf2405b8f653edddfc69fec02a + checksum: b053c6b7158d0ee7a3e55391bfd8be956fc5380211ca586b3a252007845e119540fb40efcc438975eaebc5ef25f46973f7ff4d9543c66e14ebd992957e0299b7 languageName: node linkType: hard @@ -27627,6 +28413,13 @@ __metadata: languageName: node linkType: hard +"hey-listen@npm:^1.0.8": + version: 1.0.8 + resolution: "hey-listen@npm:1.0.8" + checksum: 6bad60b367688f5348e25e7ca3276a74b59ac5a09b0455e6ff8ab7d4a9e38cd2116c708a7dcd8a954d27253ce1d8717ec891d175723ea739885b828cf44e4072 + languageName: node + linkType: hard + "highlight.js@npm:^10.1.0, highlight.js@npm:^10.4.1, highlight.js@npm:^10.6.0, highlight.js@npm:^10.7.2, highlight.js@npm:~10.7.0": version: 10.7.3 resolution: "highlight.js@npm:10.7.3" @@ -35259,6 +36052,18 @@ __metadata: languageName: node linkType: hard +"popmotion@npm:11.0.3": + version: 11.0.3 + resolution: "popmotion@npm:11.0.3" + dependencies: + framesync: 6.0.1 + hey-listen: ^1.0.8 + style-value-types: 5.0.0 + tslib: ^2.1.0 + checksum: 9fe7d03b4ec0e85bfb9dadc23b745147bfe42e16f466ba06e6327197d0e38b72015afc2f918a8051dedc3680310417f346ffdc463be6518e2e92e98f48e30268 + languageName: node + linkType: hard + "popper.js@npm:1.16.1-lts": version: 1.16.1-lts resolution: "popper.js@npm:1.16.1-lts" @@ -36867,6 +37672,41 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll-bar@npm:^2.3.3": + version: 2.3.4 + resolution: "react-remove-scroll-bar@npm:2.3.4" + dependencies: + react-style-singleton: ^2.2.1 + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: b5ce5f2f98d65c97a3e975823ae4043a4ba2a3b63b5ba284b887e7853f051b5cd6afb74abde6d57b421931c52f2e1fdbb625dc858b1cb5a32c27c14ab85649d4 + languageName: node + linkType: hard + +"react-remove-scroll@npm:2.5.5": + version: 2.5.5 + resolution: "react-remove-scroll@npm:2.5.5" + dependencies: + react-remove-scroll-bar: ^2.3.3 + react-style-singleton: ^2.2.1 + tslib: ^2.1.0 + use-callback-ref: ^1.3.0 + use-sidecar: ^1.1.2 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 2c7fe9cbd766f5e54beb4bec2e2efb2de3583037b23fef8fa511ab426ed7f1ae992382db5acd8ab5bfb030a4b93a06a2ebca41377d6eeaf0e6791bb0a59616a4 + languageName: node + linkType: hard + "react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5": version: 3.0.5 resolution: "react-resizable@npm:3.0.5" @@ -37000,6 +37840,23 @@ __metadata: languageName: node linkType: hard +"react-style-singleton@npm:^2.2.1": + version: 2.2.1 + resolution: "react-style-singleton@npm:2.2.1" + dependencies: + get-nonce: ^1.0.0 + invariant: ^2.2.4 + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 7ee8ef3aab74c7ae1d70ff34a27643d11ba1a8d62d072c767827d9ff9a520905223e567002e0bf6c772929d8ea1c781a3ba0cc4a563e92b1e3dc2eaa817ecbe8 + languageName: node + linkType: hard + "react-syntax-highlighter@npm:^15.4.5, react-syntax-highlighter@npm:^15.5.0": version: 15.5.0 resolution: "react-syntax-highlighter@npm:15.5.0" @@ -39863,6 +40720,16 @@ __metadata: languageName: node linkType: hard +"style-value-types@npm:5.0.0": + version: 5.0.0 + resolution: "style-value-types@npm:5.0.0" + dependencies: + hey-listen: ^1.0.8 + tslib: ^2.1.0 + checksum: 16d198302cd102edf9dba94e7752a2364c93b1eaa5cc7c32b42b28eef4af4ccb5149a3f16bc2a256adc02616a2404f4612bd15f3081c1e8ca06132cae78be6c0 + languageName: node + linkType: hard + "styled-components@npm:^5.3.3": version: 5.3.11 resolution: "styled-components@npm:5.3.11" @@ -41570,6 +42437,21 @@ __metadata: languageName: node linkType: hard +"use-callback-ref@npm:^1.3.0": + version: 1.3.0 + resolution: "use-callback-ref@npm:1.3.0" + dependencies: + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 7913df383a5a6fcb399212eedefaac2e0c6f843555202d4e3010bac3848afe38ecaa3d0d6500ad1d936fbeffd637e6c517e68edb024af5e6beca7f27f3ce7b21 + languageName: node + linkType: hard + "use-composed-ref@npm:^1.3.0": version: 1.3.0 resolution: "use-composed-ref@npm:1.3.0" @@ -41648,6 +42530,22 @@ __metadata: languageName: node linkType: hard +"use-sidecar@npm:^1.1.2": + version: 1.1.2 + resolution: "use-sidecar@npm:1.1.2" + dependencies: + detect-node-es: ^1.1.0 + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 925d1922f9853e516eaad526b6fed1be38008073067274f0ecc3f56b17bb8ab63480140dd7c271f94150027c996cea4efe83d3e3525e8f3eda22055f6a39220b + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" @@ -41974,10 +42872,10 @@ __metadata: languageName: node linkType: hard -"vscode-languageserver-types@npm:^3.15.1": - version: 3.15.1 - resolution: "vscode-languageserver-types@npm:3.15.1" - checksum: 28c1cb0d5b7ad7c719015a6eb5f774c000fe68c41bbd4a3fd0cbe6d862f27eec075d6bfb14c9d86c22d0e8711c2df8194295bf1e7d6ac0c359cab348c5e14887 +"vscode-languageserver-types@npm:^3.17.1": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 79b420e7576398d396579ca3a461c9ed70e78db4403cd28bbdf4d3ed2b66a2b4114031172e51fad49f0baa60a2180132d7cb2ea35aa3157d7af3c325528210ac languageName: node linkType: hard From ec61acd4997d0385ebf150a5722ae229851594f6 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Fri, 20 Oct 2023 13:21:48 -0400 Subject: [PATCH 10/70] Changed from patch to minor Signed-off-by: Taras Mankovski --- .changeset/four-files-behave.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/four-files-behave.md b/.changeset/four-files-behave.md index 3e218aa7e8..0430bd98d8 100644 --- a/.changeset/four-files-behave.md +++ b/.changeset/four-files-behave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-graphiql': patch +'@backstage/plugin-graphiql': minor --- -Upgrade to GraphiQL 3.0.6 +Upgrade to GraphiQL to 3.0.6 From edccfd966200cac8e7fb9b088c7d3830f20baa91 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:34:08 +0000 Subject: [PATCH 11/70] chore(deps): update github/codeql-action action to v2.22.4 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- .github/workflows/sync_snyk-monitor.yml | 2 +- .github/workflows/verify_codeql.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c9078f0086..0f99453bba 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -66,6 +66,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 with: sarif_file: results.sarif diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 6450b1bd51..84b2b3387b 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -58,6 +58,6 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report - uses: github/codeql-action/upload-sarif@v2.21.8 + uses: github/codeql-action/upload-sarif@v2.22.4 with: sarif_file: snyk.sarif diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 666bd5f0f4..fb29fb78e1 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -55,7 +55,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2.21.8 + uses: github/codeql-action/init@v2.22.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2.21.8 + uses: github/codeql-action/autobuild@v2.22.4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2.21.8 + uses: github/codeql-action/analyze@v2.22.4 From 361bb34d8eeb6c2d1bdfcd5521744ca2f0594eef Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:03:17 -0500 Subject: [PATCH 12/70] Refactored annotation values Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- .changeset/poor-seahorses-rush.md | 5 + .../EntityPageAzurePipelines.tsx | 4 +- .../src/components/ReadmeCard/ReadmeCard.tsx | 18 +- plugins/azure-devops/src/constants.ts | 2 + plugins/azure-devops/src/hooks/index.ts | 1 - plugins/azure-devops/src/hooks/useGitTags.ts | 11 +- .../src/hooks/useProjectRepoFromEntity.ts | 47 ---- .../azure-devops/src/hooks/usePullRequests.ts | 11 +- .../azure-devops/src/hooks/useRepoBuilds.ts | 11 +- .../getAnnotationValuesFromEntity.test.ts | 230 ++++++++++++++++++ ...ty.ts => getAnnotationValuesFromEntity.ts} | 29 ++- plugins/azure-devops/src/utils/index.ts | 2 +- 12 files changed, 292 insertions(+), 79 deletions(-) create mode 100644 .changeset/poor-seahorses-rush.md delete mode 100644 plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts create mode 100644 plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts rename plugins/azure-devops/src/utils/{getAnnotationFromEntity.ts => getAnnotationValuesFromEntity.ts} (72%) diff --git a/.changeset/poor-seahorses-rush.md b/.changeset/poor-seahorses-rush.md new file mode 100644 index 0000000000..55313f706d --- /dev/null +++ b/.changeset/poor-seahorses-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops': patch +--- + +Consolidated getting the annotation values into a single function to help with future changes diff --git a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx index ad278e3fc4..02a61a9a9a 100644 --- a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx @@ -16,14 +16,14 @@ import { BuildTable } from '../BuildTable/BuildTable'; import React from 'react'; -import { getAnnotationFromEntity } from '../../utils/getAnnotationFromEntity'; +import { getAnnotationValuesFromEntity } from '../../utils/getAnnotationValuesFromEntity'; import { useBuildRuns } from '../../hooks/useBuildRuns'; import { useEntity } from '@backstage/plugin-catalog-react'; export const EntityPageAzurePipelines = (props: { defaultLimit?: number }) => { const { entity } = useEntity(); - const { project, repo, definition } = getAnnotationFromEntity(entity); + const { project, repo, definition } = getAnnotationValuesFromEntity(entity); const { items, loading, error } = useBuildRuns( project, diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index d06726e929..d9afe8db5a 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -23,11 +23,11 @@ import { ErrorPanel, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useProjectRepoFromEntity } from '../../hooks'; import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; import { azureDevOpsApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; +import { getAnnotationValuesFromEntity } from '../../utils'; const useStyles = makeStyles(theme => ({ readMe: { @@ -87,16 +87,14 @@ export const ReadmeCard = (props: Props) => { const classes = useStyles(); const api = useApi(azureDevOpsApiRef); const { entity } = useEntity(); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { loading, error, value } = useAsync( - () => - api.getReadme({ - project, - repo, - }), - [api, project, repo, entity], - ); + const { loading, error, value } = useAsync(async () => { + if (repo) { + return await api.getReadme({ project, repo }); + } + return undefined; + }, [api, project, repo, entity]); if (loading) { return ; diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts index 95e5be4930..765f5a965b 100644 --- a/plugins/azure-devops/src/constants.ts +++ b/plugins/azure-devops/src/constants.ts @@ -16,6 +16,8 @@ export const AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION = 'dev.azure.com/build-definition'; +export const AZURE_DEVOPS_ORGANIZATION_ANNOTATION = + 'dev.azure.com/organization'; export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; export const AZURE_DEVOPS_DEFAULT_TOP: number = 10; diff --git a/plugins/azure-devops/src/hooks/index.ts b/plugins/azure-devops/src/hooks/index.ts index a3864c21f7..0b745ba586 100644 --- a/plugins/azure-devops/src/hooks/index.ts +++ b/plugins/azure-devops/src/hooks/index.ts @@ -16,7 +16,6 @@ export * from './useAllTeams'; export * from './useDashboardPullRequests'; -export * from './useProjectRepoFromEntity'; export * from './usePullRequests'; export * from './useRepoBuilds'; export * from './useUserEmail'; diff --git a/plugins/azure-devops/src/hooks/useGitTags.ts b/plugins/azure-devops/src/hooks/useGitTags.ts index b940a7105d..f36f9224aa 100644 --- a/plugins/azure-devops/src/hooks/useGitTags.ts +++ b/plugins/azure-devops/src/hooks/useGitTags.ts @@ -20,7 +20,7 @@ import { Entity } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; -import { useProjectRepoFromEntity } from './useProjectRepoFromEntity'; +import { getAnnotationValuesFromEntity } from '../utils'; export function useGitTags(entity: Entity): { items?: GitTag[]; @@ -28,10 +28,13 @@ export function useGitTags(entity: Entity): { error?: Error; } { const api = useApi(azureDevOpsApiRef); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { value, loading, error } = useAsync(() => { - return api.getGitTags(project, repo); + const { value, loading, error } = useAsync(async () => { + if (repo) { + return await api.getGitTags(project, repo); + } + return undefined; }, [api, project, repo]); return { diff --git a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts deleted file mode 100644 index b484bca009..0000000000 --- a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Entity } from '@backstage/catalog-model'; -import { AZURE_DEVOPS_REPO_ANNOTATION } from '../constants'; - -export function useProjectRepoFromEntity(entity: Entity): { - project: string; - repo: string; -} { - const [project, repo] = ( - entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION] ?? '' - ).split('/'); - - if (!project && !repo) { - throw new Error( - 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', - ); - } - - if (!project) { - throw new Error( - 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', - ); - } - - if (!repo) { - throw new Error( - 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', - ); - } - - return { project, repo }; -} diff --git a/plugins/azure-devops/src/hooks/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts index 9ddca9650b..c412836ae5 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.ts +++ b/plugins/azure-devops/src/hooks/usePullRequests.ts @@ -25,7 +25,7 @@ import { Entity } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; -import { useProjectRepoFromEntity } from './useProjectRepoFromEntity'; +import { getAnnotationValuesFromEntity } from '../utils'; export function usePullRequests( entity: Entity, @@ -44,10 +44,13 @@ export function usePullRequests( }; const api = useApi(azureDevOpsApiRef); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { value, loading, error } = useAsync(() => { - return api.getPullRequests(project, repo, options); + const { value, loading, error } = useAsync(async () => { + if (repo) { + return await api.getPullRequests(project, repo, options); + } + return undefined; }, [api, project, repo, top, status]); return { diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts index e0fe8c2093..4ba88bd5fb 100644 --- a/plugins/azure-devops/src/hooks/useRepoBuilds.ts +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -24,7 +24,7 @@ import { Entity } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; -import { useProjectRepoFromEntity } from './useProjectRepoFromEntity'; +import { getAnnotationValuesFromEntity } from '../utils'; export function useRepoBuilds( entity: Entity, @@ -40,10 +40,13 @@ export function useRepoBuilds( }; const api = useApi(azureDevOpsApiRef); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { value, loading, error } = useAsync(() => { - return api.getRepoBuilds(project, repo, options); + const { value, loading, error } = useAsync(async () => { + if (repo) { + return await api.getRepoBuilds(project, repo, options); + } + return undefined; }, [api, project, repo, entity]); return { diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts new file mode 100644 index 0000000000..a34dab9db5 --- /dev/null +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { getAnnotationValuesFromEntity } from './getAnnotationValuesFromEntity'; + +describe('getAnnotationValuesFromEntity', () => { + describe('with valid project-repo annotation', () => { + it('should return project and repo', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual('repoName'); + expect(definition).toEqual(undefined); + expect(org).toEqual(undefined); + }); + }); + + describe('with invalid project-repo annotation', () => { + it('should throw incorrect format error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'project', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', + ); + }); + }); + + describe('with project-repo annotation missing project', () => { + it('should throw missing project error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': '/repo', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + }); + }); + + describe('with project-repo annotation missing repo', () => { + it('should throw missing repo error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'project/', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + }); + }); + + describe('with valid project and build-definition annotations', () => { + it('should return project and definition', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-build-definition', + annotations: { + 'dev.azure.com/project': 'projectName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual(undefined); + expect(definition).toEqual('buildDefinitionName'); + expect(org).toEqual(undefined); + }); + }); + + describe('with only project annotation', () => { + it('should return project and definition', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project', + annotations: { + 'dev.azure.com/project': 'projectName', + }, + }, + }; + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/build-definition was not found', + ); + }); + }); + + describe('with only build-definition annotation', () => { + it('should return project and definition', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'build-definition', + annotations: { + 'dev.azure.com/build-definition': 'buildDefinitionName', + }, + }, + }; + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/project was not found', + ); + }); + }); + + describe('with valid project-repo and org annotations', () => { + it('should return project, repo, and org', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + 'dev.azure.com/organization': 'organizationName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual('repoName'); + expect(definition).toEqual(undefined); + expect(org).toEqual('organizationName'); + }); + }); + + describe('with valid project, build-definition, and org annotations', () => { + it('should return project, definition, and org', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-build-definition', + annotations: { + 'dev.azure.com/project': 'projectName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + 'dev.azure.com/organization': 'organizationName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual(undefined); + expect(definition).toEqual('buildDefinitionName'); + expect(org).toEqual('organizationName'); + }); + }); +}); diff --git a/plugins/azure-devops/src/utils/getAnnotationFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts similarity index 72% rename from plugins/azure-devops/src/utils/getAnnotationFromEntity.ts rename to plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 41cc1e6628..1070983b27 100644 --- a/plugins/azure-devops/src/utils/getAnnotationFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -16,23 +16,28 @@ import { AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION, + AZURE_DEVOPS_ORGANIZATION_ANNOTATION, AZURE_DEVOPS_PROJECT_ANNOTATION, AZURE_DEVOPS_REPO_ANNOTATION, } from '../constants'; import { Entity } from '@backstage/catalog-model'; -export function getAnnotationFromEntity(entity: Entity): { +export function getAnnotationValuesFromEntity(entity: Entity): { project: string; repo?: string; definition?: string; + org?: string; } { + const org = + entity.metadata.annotations?.[AZURE_DEVOPS_ORGANIZATION_ANNOTATION]; + const annotation = entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; if (annotation) { const { project, repo } = getProjectRepo(annotation); const definition = undefined; - return { project, repo, definition }; + return { project, repo, definition, org }; } const project = @@ -50,20 +55,32 @@ export function getAnnotationFromEntity(entity: Entity): { } const repo = undefined; - return { project, repo, definition }; + return { project, repo, definition, org }; } function getProjectRepo(annotation: string): { project: string; repo: string; } { - const [project, repo] = annotation.split('/'); - - if (!project && !repo) { + if (!annotation.includes('/')) { throw new Error( 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', ); } + const [project, repo] = annotation.split('/'); + + if (!project) { + throw new Error( + 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + } + + if (!repo) { + throw new Error( + 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + } + return { project, repo }; } diff --git a/plugins/azure-devops/src/utils/index.ts b/plugins/azure-devops/src/utils/index.ts index 8655b261c3..998bb0e23d 100644 --- a/plugins/azure-devops/src/utils/index.ts +++ b/plugins/azure-devops/src/utils/index.ts @@ -17,4 +17,4 @@ export * from './arrayHas'; export * from './equalsIgnoreCase'; export * from './getDurationFromDates'; -export * from './getAnnotationFromEntity'; +export * from './getAnnotationValuesFromEntity'; From 9987f11687fe9ce0d4f95dfd131f1779f5b7d517 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:12:48 -0500 Subject: [PATCH 13/70] Refactor to also return host Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- plugins/azure-devops/src/constants.ts | 3 +- .../getAnnotationValuesFromEntity.test.ts | 100 +++++++++++++++--- .../utils/getAnnotationValuesFromEntity.ts | 61 ++++++++--- 3 files changed, 137 insertions(+), 27 deletions(-) diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts index 765f5a965b..aba0ec093a 100644 --- a/plugins/azure-devops/src/constants.ts +++ b/plugins/azure-devops/src/constants.ts @@ -16,8 +16,7 @@ export const AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION = 'dev.azure.com/build-definition'; -export const AZURE_DEVOPS_ORGANIZATION_ANNOTATION = - 'dev.azure.com/organization'; +export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org'; export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; export const AZURE_DEVOPS_DEFAULT_TOP: number = 10; diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index a34dab9db5..5584567188 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -31,11 +31,12 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual('repoName'); expect(definition).toEqual(undefined); + expect(host).toEqual(undefined); expect(org).toEqual(undefined); }); }); @@ -126,17 +127,18 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual(undefined); expect(definition).toEqual('buildDefinitionName'); + expect(host).toEqual(undefined); expect(org).toEqual(undefined); }); }); describe('with only project annotation', () => { - it('should return project and definition', () => { + it('should should throw annotation not found error', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -159,7 +161,7 @@ describe('getAnnotationValuesFromEntity', () => { }); describe('with only build-definition annotation', () => { - it('should return project and definition', () => { + it('should should throw annotation not found error', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -181,8 +183,8 @@ describe('getAnnotationValuesFromEntity', () => { }); }); - describe('with valid project-repo and org annotations', () => { - it('should return project, repo, and org', () => { + describe('with valid project-repo and host-org annotations', () => { + it('should return project, repo, host, and org', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -191,21 +193,22 @@ describe('getAnnotationValuesFromEntity', () => { name: 'project-repo', annotations: { 'dev.azure.com/project-repo': 'projectName/repoName', - 'dev.azure.com/organization': 'organizationName', + 'dev.azure.com/host-org': 'hostName/organizationName', }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual('repoName'); expect(definition).toEqual(undefined); + expect(host).toEqual('hostName'); expect(org).toEqual('organizationName'); }); }); - describe('with valid project, build-definition, and org annotations', () => { - it('should return project, definition, and org', () => { + describe('with valid project, build-definition, and host-org annotations', () => { + it('should return project, definition, host and org', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -215,16 +218,89 @@ describe('getAnnotationValuesFromEntity', () => { annotations: { 'dev.azure.com/project': 'projectName', 'dev.azure.com/build-definition': 'buildDefinitionName', - 'dev.azure.com/organization': 'organizationName', + 'dev.azure.com/host-org': 'hostName/organizationName', }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual(undefined); expect(definition).toEqual('buildDefinitionName'); + expect(host).toEqual('hostName'); expect(org).toEqual('organizationName'); }); }); + + describe('with invalid host-org annotation', () => { + it('should throw incorrect format error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'host-org', + annotations: { + 'dev.azure.com/host-org': 'host', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/host-org was not in the correct format: /', + ); + }); + }); + + describe('with host-rg annotation missing host', () => { + it('should throw missing project error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'host-org', + annotations: { + 'dev.azure.com/host-org': '/org', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Host for annotation dev.azure.com/host-org was not found; expected format is: /', + ); + }); + }); + + describe('with host-org annotation missing org', () => { + it('should throw missing repo error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'host-org', + annotations: { + 'dev.azure.com/host-org': 'host/', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Organization for annotation dev.azure.com/host-org was not found; expected format is: /', + ); + }); + }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 1070983b27..8ce096d3c5 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -16,7 +16,7 @@ import { AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION, - AZURE_DEVOPS_ORGANIZATION_ANNOTATION, + AZURE_DEVOPS_HOST_ORG_ANNOTATION, AZURE_DEVOPS_PROJECT_ANNOTATION, AZURE_DEVOPS_REPO_ANNOTATION, } from '../constants'; @@ -27,35 +27,39 @@ export function getAnnotationValuesFromEntity(entity: Entity): { project: string; repo?: string; definition?: string; + host?: string; org?: string; } { - const org = - entity.metadata.annotations?.[AZURE_DEVOPS_ORGANIZATION_ANNOTATION]; + const hostOrgAnnotation = + entity.metadata.annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION]; + const { host, org } = getHostOrg(hostOrgAnnotation); - const annotation = + const projectRepoAnnotation = entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; - if (annotation) { - const { project, repo } = getProjectRepo(annotation); + if (projectRepoAnnotation) { + const { project, repo } = getProjectRepo(projectRepoAnnotation); const definition = undefined; - return { project, repo, definition, org }; + return { project, repo, definition, host, org }; } const project = entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION]; if (!project) { - throw new Error('Value for annotation dev.azure.com/project was not found'); + throw new Error( + `Value for annotation ${AZURE_DEVOPS_PROJECT_ANNOTATION} was not found`, + ); } const definition = entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION]; if (!definition) { throw new Error( - 'Value for annotation dev.azure.com/build-definition was not found', + `Value for annotation ${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION} was not found`, ); } const repo = undefined; - return { project, repo, definition, org }; + return { project, repo, definition, host, org }; } function getProjectRepo(annotation: string): { @@ -64,7 +68,7 @@ function getProjectRepo(annotation: string): { } { if (!annotation.includes('/')) { throw new Error( - 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', + `Value for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not in the correct format: /`, ); } @@ -72,15 +76,46 @@ function getProjectRepo(annotation: string): { if (!project) { throw new Error( - 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + `Project Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, ); } if (!repo) { throw new Error( - 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + `Repo Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, ); } return { project, repo }; } + +function getHostOrg(annotation?: string): { + host?: string; + org?: string; +} { + if (!annotation) { + return { host: undefined, org: undefined }; + } + + if (!annotation.includes('/')) { + throw new Error( + `Value for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not in the correct format: /`, + ); + } + + const [host, org] = annotation.split('/'); + + if (!host) { + throw new Error( + `Host for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, + ); + } + + if (!org) { + throw new Error( + `Organization for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, + ); + } + + return { host, org }; +} From 904d7f12408d24e98e26df5a116cbb36be0a93c1 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:59:49 -0500 Subject: [PATCH 14/70] Initial improvements from feedback Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- .../getAnnotationValuesFromEntity.test.ts | 62 ++++++++++--------- .../utils/getAnnotationValuesFromEntity.ts | 36 ++++++----- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index 5584567188..0d7a11f48b 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -31,13 +31,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual('repoName'); - expect(definition).toEqual(undefined); - expect(host).toEqual(undefined); - expect(org).toEqual(undefined); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: undefined, + host: undefined, + org: undefined, + }); }); }); @@ -127,13 +128,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual(undefined); - expect(definition).toEqual('buildDefinitionName'); - expect(host).toEqual(undefined); - expect(org).toEqual(undefined); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: undefined, + definition: 'buildDefinitionName', + host: undefined, + org: undefined, + }); }); }); @@ -197,13 +199,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual('repoName'); - expect(definition).toEqual(undefined); - expect(host).toEqual('hostName'); - expect(org).toEqual('organizationName'); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: undefined, + host: 'hostName', + org: 'organizationName', + }); }); }); @@ -222,13 +225,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual(undefined); - expect(definition).toEqual('buildDefinitionName'); - expect(host).toEqual('hostName'); - expect(org).toEqual('organizationName'); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: undefined, + definition: 'buildDefinitionName', + host: 'hostName', + org: 'organizationName', + }); }); }); @@ -256,7 +260,7 @@ describe('getAnnotationValuesFromEntity', () => { }); }); - describe('with host-rg annotation missing host', () => { + describe('with host-org annotation missing host', () => { it('should throw missing project error', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 8ce096d3c5..7099352524 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -30,16 +30,16 @@ export function getAnnotationValuesFromEntity(entity: Entity): { host?: string; org?: string; } { - const hostOrgAnnotation = - entity.metadata.annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION]; - const { host, org } = getHostOrg(hostOrgAnnotation); + const { host, org } = getHostOrg(entity.metadata.annotations); - const projectRepoAnnotation = - entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; - if (projectRepoAnnotation) { - const { project, repo } = getProjectRepo(projectRepoAnnotation); - const definition = undefined; - return { project, repo, definition, host, org }; + const projectRepoValues = getProjectRepo(entity.metadata.annotations); + if (projectRepoValues.project && projectRepoValues.repo) { + return { + project: projectRepoValues.project, + repo: projectRepoValues.repo, + host, + org, + }; } const project = @@ -57,15 +57,18 @@ export function getAnnotationValuesFromEntity(entity: Entity): { `Value for annotation ${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION} was not found`, ); } - - const repo = undefined; - return { project, repo, definition, host, org }; + return { project, definition, host, org }; } -function getProjectRepo(annotation: string): { - project: string; - repo: string; +function getProjectRepo(annotations?: Record): { + project?: string; + repo?: string; } { + const annotation = annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; + if (!annotation) { + return { project: undefined, repo: undefined }; + } + if (!annotation.includes('/')) { throw new Error( `Value for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not in the correct format: /`, @@ -89,10 +92,11 @@ function getProjectRepo(annotation: string): { return { project, repo }; } -function getHostOrg(annotation?: string): { +function getHostOrg(annotations?: Record): { host?: string; org?: string; } { + const annotation = annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION]; if (!annotation) { return { host: undefined, org: undefined }; } From 9bc4d2b4497095da1a0142949339548c07679b8f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 20 Oct 2023 16:05:46 -0500 Subject: [PATCH 15/70] Added EntityAzureReadmeCard to help with testing Signed-off-by: Andre Wanlin --- packages/app/src/components/catalog/EntityPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 81af3b91e7..ecc1f008b5 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -41,6 +41,7 @@ import { EntityAzurePullRequestsContent, isAzureDevOpsAvailable, isAzurePipelinesAvailable, + EntityAzureReadmeCard, } from '@backstage/plugin-azure-devops'; import { isOctopusDeployAvailable, @@ -415,6 +416,14 @@ const overviewContent = ( + + + + + + + + From 36123575ad64d91181e91eb712d152b0b971c577 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 25 May 2023 11:53:34 +0200 Subject: [PATCH 16/70] catalog-react: add reduceBackendCatalogFilters Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/utils/filters.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index 109e864c52..5ab6e43593 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '../types'; +import { EntityKindFilter, EntityTypeFilter } from '../filters'; export function reduceCatalogFilters( filters: EntityFilter[], @@ -28,6 +29,24 @@ export function reduceCatalogFilters( }, {} as Record); } +export function reduceBackendCatalogFilters(filters: EntityFilter[]) { + const backendCatalogFilters: Record< + string, + string | symbol | (string | symbol)[] + > = {}; + + filters.forEach(filter => { + if ( + filter instanceof EntityKindFilter || + filter instanceof EntityTypeFilter + ) { + Object.assign(backendCatalogFilters, filter.getCatalogFilters()); + } + }); + + return backendCatalogFilters; +} + export function reduceEntityFilters( filters: EntityFilter[], ): (entity: Entity) => boolean { From c9f2a54d71411a253fe2b4a665ed1d427d3defc2 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 25 May 2023 11:57:14 +0200 Subject: [PATCH 17/70] catalog-react: pick kind and type filters as backend filters Signed-off-by: Vincenzo Scamporlino --- .../catalog-react/src/hooks/useEntityListProvider.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index b4c464a26c..b7ee8a94ed 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -41,16 +41,17 @@ import { EntityTypeFilter, UserListFilter, EntityNamespaceFilter, + UserOwnersFilter, } from '../filters'; import { EntityFilter } from '../types'; -import { reduceCatalogFilters, reduceEntityFilters } from '../utils'; +import { reduceBackendCatalogFilters, reduceEntityFilters } from '../utils'; import { useApi } from '@backstage/core-plugin-api'; /** @public */ export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; - user?: UserListFilter; + user?: UserListFilter | UserOwnersFilter; owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; @@ -156,8 +157,8 @@ export const EntityListProvider = ( async () => { const compacted = compact(Object.values(requestedFilters)); const entityFilter = reduceEntityFilters(compacted); - const backendFilter = reduceCatalogFilters(compacted); - const previousBackendFilter = reduceCatalogFilters( + const backendFilter = reduceBackendCatalogFilters(compacted); + const previousBackendFilter = reduceBackendCatalogFilters( compact(Object.values(outputState.appliedFilters)), ); From ae2f2dd18d99c1b23ed25db769d9470e3c617b54 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 25 May 2023 11:59:47 +0200 Subject: [PATCH 18/70] catalog-react: add getCatalogFilters methods for backend filtering Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/filters.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index c717a35202..1213ab9b7e 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -72,6 +72,10 @@ export class EntityTagFilter implements EntityFilter { return this.values.every(v => (entity.metadata.tags ?? []).includes(v)); } + getCatalogFilters(): Record { + return { 'metadata.tags': this.values }; + } + toQueryValue(): string[] { return this.values; } @@ -136,6 +140,10 @@ export class EntityOwnerFilter implements EntityFilter { }, [] as string[]); } + getCatalogFilters(): Record { + return { 'relations.ownedBy': this.values }; + } + filterEntity(entity: Entity): boolean { return this.values.some(v => getEntityRelations(entity, RELATION_OWNED_BY).some( @@ -160,6 +168,10 @@ export class EntityOwnerFilter implements EntityFilter { export class EntityLifecycleFilter implements EntityFilter { constructor(readonly values: string[]) {} + getCatalogFilters(): Record { + return { 'spec.lifecycle': this.values }; + } + filterEntity(entity: Entity): boolean { return this.values.some(v => entity.spec?.lifecycle === v); } @@ -176,6 +188,9 @@ export class EntityLifecycleFilter implements EntityFilter { export class EntityNamespaceFilter implements EntityFilter { constructor(readonly values: string[]) {} + getCatalogFilters(): Record { + return { 'spec.lifecycle': this.values }; + } filterEntity(entity: Entity): boolean { return this.values.some(v => entity.metadata.namespace === v); } @@ -218,6 +233,11 @@ export class UserListFilter implements EntityFilter { */ export class EntityOrphanFilter implements EntityFilter { constructor(readonly value: boolean) {} + + getCatalogFilters(): Record { + return { 'metadata.annotations.backstage.io/orphan': String(this.value) }; + } + filterEntity(entity: Entity): boolean { const orphan = entity.metadata.annotations?.['backstage.io/orphan']; return orphan !== undefined && this.value.toString() === orphan; @@ -230,6 +250,10 @@ export class EntityOrphanFilter implements EntityFilter { */ export class EntityErrorFilter implements EntityFilter { constructor(readonly value: boolean) {} + + // TODO(vinzscam): is it possible to implement + // getCatalogFilters? ask mammals + filterEntity(entity: Entity): boolean { const error = ((entity as AlphaEntity)?.status?.items?.length as number) > 0; From 8bae84e5f47597a1ebcb1c783b00aa91aa6afb19 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 25 May 2023 12:00:07 +0200 Subject: [PATCH 19/70] catalog-react: introduce UserOwnersFilter Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/filters.ts | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 1213ab9b7e..a5a39f9b02 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -200,8 +200,54 @@ export class EntityNamespaceFilter implements EntityFilter { } } +/** + * @public + */ +export class UserOwnersFilter implements EntityFilter { + private constructor( + readonly value: UserListFilterKind, + readonly refs?: string[], + ) {} + + static owned(ownershipEntityRefs: string[]) { + return new UserOwnersFilter('owned', ownershipEntityRefs); + } + + static all() { + return new UserOwnersFilter('all'); + } + + static starred(starredEntityRefs: string[]) { + return new UserOwnersFilter('starred', starredEntityRefs); + } + + getCatalogFilters(): Record { + if (this.value === 'owned') { + return { 'relations.ownedBy': this.refs ?? [] }; + } + if (this.value === 'starred') { + return { + 'metadata.name': this.refs?.map(e => parseEntityRef(e).name) ?? [], + }; + } + return {}; + } + + filterEntity(entity: Entity) { + if (this.value === 'starred') { + return this.refs?.includes(stringifyEntityRef(entity)) ?? true; + } + return true; + } + + toQueryValue(): string { + return this.value; + } +} + /** * Filters entities based on whatever the user has starred or owns them. + * @deprecated use UserOwnersFilter * @public */ export class UserListFilter implements EntityFilter { From 58b6d860d34653be8b968c3f60825d520e28f659 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 25 May 2023 12:00:59 +0200 Subject: [PATCH 20/70] catalog-react: add useIsOwnedEntity hook Signed-off-by: Vincenzo Scamporlino --- .../catalog-react/src/hooks/useEntityOwnership.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index f8a37a2c83..1f1e200634 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -46,9 +46,15 @@ export function useEntityOwnership(): { return ownershipEntityRefs; }, []); - const isOwnedEntity = useMemo(() => { + const isOwnedEntity = useIsOwnedEntity(refs); + + return useMemo(() => ({ loading, isOwnedEntity }), [loading, isOwnedEntity]); +} + +export function useIsOwnedEntity(refs?: string[]) { + return useMemo(() => { const myOwnerRefs = new Set(refs ?? []); - return (entity: Entity) => { + const isOwnedEntity = (entity: Entity) => { const entityOwnerRefs = getEntityRelations(entity, RELATION_OWNED_BY).map( stringifyEntityRef, ); @@ -59,7 +65,6 @@ export function useEntityOwnership(): { } return false; }; + return isOwnedEntity; }, [refs]); - - return useMemo(() => ({ loading, isOwnedEntity }), [loading, isOwnedEntity]); } From 1b31deed8f7d136993e180b86e27fee03183b4c1 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 25 May 2023 12:03:32 +0200 Subject: [PATCH 21/70] catalog-react: decouple UserListPicker from backendEntities Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/api-report.md | 35 +- .../UserListPicker/UserListPicker.test.tsx | 695 +++++++++++++----- .../UserListPicker/UserListPicker.tsx | 132 ++-- .../UserListPicker/useAllEntitiesCount.ts | 61 ++ .../UserListPicker/useOwnedEntitiesCount.ts | 107 +++ .../UserListPicker/useStarredEntitiesCount.ts | 80 ++ 6 files changed, 879 insertions(+), 231 deletions(-) create mode 100644 plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts create mode 100644 plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts create mode 100644 plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index a72df6bb3d..3faa5524c9 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -142,7 +142,7 @@ export const columnFactories: Readonly<{ export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; - user?: UserListFilter; + user?: UserListFilter | UserOwnersFilter; owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; @@ -224,6 +224,8 @@ export class EntityLifecycleFilter implements EntityFilter { // (undocumented) filterEntity(entity: Entity): boolean; // (undocumented) + getCatalogFilters(): Record; + // (undocumented) toQueryValue(): string[]; // (undocumented) readonly values: string[]; @@ -275,6 +277,8 @@ export class EntityNamespaceFilter implements EntityFilter { // (undocumented) filterEntity(entity: Entity): boolean; // (undocumented) + getCatalogFilters(): Record; + // (undocumented) toQueryValue(): string[]; // (undocumented) readonly values: string[]; @@ -289,6 +293,8 @@ export class EntityOrphanFilter implements EntityFilter { // (undocumented) filterEntity(entity: Entity): boolean; // (undocumented) + getCatalogFilters(): Record; + // (undocumented) readonly value: boolean; } @@ -297,6 +303,8 @@ export class EntityOwnerFilter implements EntityFilter { constructor(values: string[]); // (undocumented) filterEntity(entity: Entity): boolean; + // (undocumented) + getCatalogFilters(): Record; toQueryValue(): string[]; // (undocumented) readonly values: string[]; @@ -447,6 +455,8 @@ export class EntityTagFilter implements EntityFilter { // (undocumented) filterEntity(entity: Entity): boolean; // (undocumented) + getCatalogFilters(): Record; + // (undocumented) toQueryValue(): string[]; // (undocumented) readonly values: string[]; @@ -620,7 +630,7 @@ export function useRelatedEntities( error: Error | undefined; }; -// @public +// @public @deprecated export class UserListFilter implements EntityFilter { constructor( value: UserListFilterKind, @@ -651,8 +661,29 @@ export const UserListPicker: ( export type UserListPickerProps = { initialFilter?: UserListFilterKind; availableFilters?: UserListFilterKind[]; + useServerSideFilters?: boolean; }; +// @public (undocumented) +export class UserOwnersFilter implements EntityFilter { + // (undocumented) + static all(): UserOwnersFilter; + // (undocumented) + filterEntity(entity: Entity): boolean; + // (undocumented) + getCatalogFilters(): Record; + // (undocumented) + static owned(ownershipEntityRefs: string[]): UserOwnersFilter; + // (undocumented) + readonly refs?: string[] | undefined; + // (undocumented) + static starred(starredEntityRefs: string[]): UserOwnersFilter; + // (undocumented) + toQueryValue(): string; + // (undocumented) + readonly value: UserListFilterKind; +} + // @public (undocumented) export function useStarredEntities(): { starredEntities: Set; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index c31381df20..1315c95061 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -16,15 +16,18 @@ import React from 'react'; import { fireEvent, render, waitFor, screen } from '@testing-library/react'; -import { - Entity, - RELATION_OWNED_BY, - UserEntity, -} from '@backstage/catalog-model'; -import { UserListPicker } from './UserListPicker'; +import { Entity, UserEntity } from '@backstage/catalog-model'; +import { UserListPicker, UserListPickerProps } from './UserListPicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityTagFilter, UserListFilter } from '../../filters'; -import { CatalogApi } from '@backstage/catalog-client'; +import { + EntityTagFilter, + UserListFilter, + UserOwnersFilter, +} from '../../filters'; +import { + CatalogApi, + QueryEntitiesInitialRequest, +} from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; @@ -35,7 +38,7 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { useEntityOwnership } from '../../hooks'; +import { MockStarredEntitiesApi, starredEntitiesApiRef } from '../../apis'; const mockUser: UserEntity = { apiVersion: 'backstage.io/v1alpha1', @@ -54,19 +57,22 @@ const mockConfigApi = { } as Partial; const mockCatalogApi = { - getEntityByRef: () => Promise.resolve(mockUser), -} as Partial; + getEntityByRef: jest.fn(), + queryEntities: jest.fn(), +} as Partial>; const mockIdentityApi = { - getUserId: () => 'testUser', - getIdToken: async () => undefined, -} as Partial; + getBackstageIdentity: jest.fn(), +} as Partial>; + +const mockStarredEntitiesApi = new MockStarredEntitiesApi(); const apis = TestApiRegistry.from( [configApiRef, mockConfigApi], [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], + [starredEntitiesApiRef, mockStarredEntitiesApi], ); const mockIsOwnedEntity = jest.fn( @@ -77,113 +83,117 @@ const mockIsStarredEntity = jest.fn( (entity: Entity) => entity.metadata.name === 'component-3', ); -jest.mock('../../hooks', () => { - const actual = jest.requireActual('../../hooks'); - return { - ...actual, - useEntityOwnership: jest.fn(() => ({ - isOwnedEntity: mockIsOwnedEntity, - })), - useStarredEntities: () => ({ - isStarredEntity: mockIsStarredEntity, - }), - }; -}); - -const backendEntities: Entity[] = [ - { - apiVersion: '1', - kind: 'Component', - metadata: { - namespace: 'namespace-1', - name: 'component-1', - tags: ['tag1'], - }, - relations: [ - { - type: RELATION_OWNED_BY, - targetRef: 'user:default/testuser', - }, - ], - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - namespace: 'namespace-2', - name: 'component-2', - tags: ['tag1'], - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - namespace: 'namespace-2', - name: 'component-3', - tags: [], - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - namespace: 'namespace-2', - name: 'component-4', - tags: [], - }, - relations: [ - { - type: RELATION_OWNED_BY, - targetRef: 'user:default/testuser', - }, - ], - }, -]; - +const ownershipEntityRefs = ['user:default/testuser']; describe('', () => { - it('renders filter groups', () => { + const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] = + async request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['relations.ownedBy'] + ) { + // owned entities + return { items: [], totalItems: 3, pageInfo: {} }; + } + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['metadata.name'] + ) { + // starred entities + return { + items: [ + { + apiVersion: '1', + kind: 'component', + metadata: { name: 'e-1', namespace: 'default' }, + }, + { + apiVersion: '1', + kind: 'component', + metadata: { name: 'e-2', namespace: 'default' }, + }, + ], + totalItems: 2, + pageInfo: {}, + }; + } + // all items + return { items: [], totalItems: 10, pageInfo: {} }; + }; + + beforeAll(() => { + mockStarredEntitiesApi.toggleStarred('component:default/e-1'); + mockStarredEntitiesApi.toggleStarred('component:default/e-2'); + }); + + beforeEach(() => { + mockCatalogApi.getEntityByRef?.mockResolvedValue(mockUser); + mockIdentityApi.getBackstageIdentity?.mockResolvedValue({ + ownershipEntityRefs, + type: 'user', + userEntityRef: 'user:default/testuser', + }); + + mockCatalogApi.queryEntities?.mockImplementation( + mockQueryEntitiesImplementation, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + it('renders filter groups', async () => { render( - + , ); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), + ); expect(screen.getByText('Personal')).toBeInTheDocument(); expect(screen.getByText('Test Company')).toBeInTheDocument(); }); - it('renders filters', () => { + it('renders filters', async () => { render( - + , ); - expect( - screen.getAllByRole('menuitem').map(({ textContent }) => textContent), - ).toEqual(['Owned 1', 'Starred 1', 'All 4']); - }); - - it('includes counts alongside each filter', async () => { - render( - - - - - , - ); - - // Material UI renders ListItemSecondaryActions outside the - // menuitem itself, so we pick off the next sibling. - await waitFor(() => { + await waitFor(() => expect( screen.getAllByRole('menuitem').map(({ textContent }) => textContent), - ).toEqual(['Owned 1', 'Starred 1', 'All 4']); + ).toEqual(['Owned 3', 'Starred 2', 'All 10']), + ); + + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: {}, + limit: 0, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { 'metadata.name': ['e-1', 'e-2'] }, + limit: 1000, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { 'relations.ownedBy': ['user:default/testuser'] }, + limit: 0, }); }); @@ -192,7 +202,6 @@ describe('', () => { @@ -204,35 +213,104 @@ describe('', () => { await waitFor(() => { expect( screen.getAllByRole('menuitem').map(({ textContent }) => textContent), - ).toEqual(['Owned 1', 'Starred 0', 'All 2']); + ).toEqual(['Owned 3', 'Starred 2', 'All 10']); + }); + + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { 'metadata.tags': ['tag1'] }, + limit: 0, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { 'metadata.name': ['e-1', 'e-2'], 'metadata.tags': ['tag1'] }, + limit: 1000, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { + 'relations.ownedBy': ['user:default/testuser'], + 'metadata.tags': ['tag1'], + }, + limit: 0, }); }); - it('respects the query parameter filter value', () => { + it('respects the query parameter filter value, legacy', async () => { const updateFilters = jest.fn(); const queryParameters = { user: 'owned' }; render( , ); - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity), + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'owned', + expect.any(Function), + expect.any(Function), + ), + }), + ); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: {}, + limit: 0, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { 'metadata.name': ['e-1', 'e-2'] }, + limit: 1000, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { + 'relations.ownedBy': ['user:default/testuser'], + }, + limit: 0, }); }); - it('updates user filter when a menuitem is selected', () => { + it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); + const queryParameters = { user: 'owned' }; render( + + + , + ); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: UserOwnersFilter.owned(ownershipEntityRefs), + }), + ); + + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: {}, + limit: 0, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { 'metadata.name': ['e-1', 'e-2'] }, + limit: 1000, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { + 'relations.ownedBy': ['user:default/testuser'], + }, + limit: 0, + }); + }); + + it('updates user filter when a menuitem is selected, legacy', async () => { + const updateFilters = jest.fn(); + render( + + , @@ -240,22 +318,45 @@ describe('', () => { fireEvent.click(screen.getByText('Starred')); - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'starred', - mockIsOwnedEntity, - mockIsStarredEntity, - ), - }); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'starred', + expect.any(Function), + expect.any(Function), + ), + }), + ); }); - it('responds to external queryParameters changes', () => { + it('updates user filter when a menuitem is selected', async () => { + const updateFilters = jest.fn(); + render( + + + + + , + ); + + fireEvent.click(screen.getByText('Starred')); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: UserOwnersFilter.starred([ + 'component:default/e-1', + 'component:default/e-2', + ]), + }), + ); + }); + + it('responds to external queryParameters changes, legacy', async () => { const updateFilters = jest.fn(); const rendered = render( ', () => { , ); - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter('all', mockIsOwnedEntity, mockIsStarredEntity), - }); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'all', + expect.any(Function), + expect.any(Function), + ), + }), + ); + rendered.rerender( ', () => { , ); expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity), + user: new UserListFilter( + 'owned', + expect.any(Function), + expect.any(Function), + ), }); }); - describe.each` - type | filterFn - ${'owned'} | ${mockIsOwnedEntity} - ${'starred'} | ${mockIsStarredEntity} - `('filter resetting for $type entities', ({ type, filterFn }) => { - let updateFilters: jest.Mock; - - const picker = (props: { loading: boolean }) => ( + it('responds to external queryParameters changes', async () => { + const updateFilters = jest.fn(); + const rendered = render( - + + + , + ); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: UserOwnersFilter.all(), + }), + ); + + rendered.rerender( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + user: UserOwnersFilter.owned(ownershipEntityRefs), + }); + }); + + describe('filter resetting', () => { + let updateFilters: jest.Mock; + + const Picker = (props: UserListPickerProps) => ( + + + ); @@ -306,57 +450,213 @@ describe('', () => { updateFilters = jest.fn(); }); - describe(`when there are no ${type} entities match the filter`, () => { - beforeEach(() => { - filterFn.mockReturnValue(false); + describe(`when there are no owned entities match the filter`, () => { + it('does not reset the filter while entities are loading', async () => { + mockCatalogApi.queryEntities?.mockImplementation( + () => new Promise(() => {}), + ); + + render(); + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), + ); + expect(updateFilters).not.toHaveBeenCalled(); }); - it('does not reset the filter while entities are loading', () => { - render(picker({ loading: true })); + it('does not reset the filter while owned entities are loading', async () => { + mockCatalogApi.queryEntities?.mockImplementation(request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['relations.ownedBy'] + ) { + return new Promise(() => {}); + } + return mockQueryEntitiesImplementation(request); + }); + render(); + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), + ); expect(updateFilters).not.toHaveBeenCalledWith({ - user: new UserListFilter( - 'all', - mockIsOwnedEntity, - mockIsStarredEntity, - ), + user: expect.any(Object), }); }); - it('does not reset the filter while owned entities are loading', () => { - const isOwnedEntity = jest.fn(() => false); - (useEntityOwnership as jest.Mock).mockReturnValueOnce({ - loading: true, - isOwnedEntity, + it('resets the filter to "all" when entities are loaded, legacy', async () => { + mockCatalogApi.queryEntities?.mockImplementation(async request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['relations.ownedBy'] + ) { + return { items: [], totalItems: 0, pageInfo: {} }; + } + return mockQueryEntitiesImplementation(request); }); - render(picker({ loading: false })); - expect(updateFilters).not.toHaveBeenCalledWith({ - user: new UserListFilter('all', isOwnedEntity, mockIsStarredEntity), - }); + render(); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'all', + expect.any(Function), + expect.any(Function), + ), + }), + ); }); - it('resets the filter to "all" when entities are loaded', () => { - render(picker({ loading: false })); - - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'all', - mockIsOwnedEntity, - mockIsStarredEntity, - ), + it('resets the filter to "all" when entities are loaded', async () => { + mockCatalogApi.queryEntities?.mockImplementation(async request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['relations.ownedBy'] + ) { + return { items: [], totalItems: 0, pageInfo: {} }; + } + return mockQueryEntitiesImplementation(request); }); + + render(); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: UserOwnersFilter.all(), + }), + ); }); }); - describe(`when there are some ${type} entities present`, () => { - beforeEach(() => { - filterFn.mockReturnValue(true); + describe(`when there are no starred entities match the filter`, () => { + it('does not reset the filter while entities are loading', async () => { + mockCatalogApi.queryEntities?.mockImplementation( + () => new Promise(() => {}), + ); + + render(); + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), + ); + expect(updateFilters).not.toHaveBeenCalled(); }); - it('does not reset the filter while entities are loading', () => { - render(picker({ loading: true })); + it('does not reset the filter while starred entities are loading', async () => { + mockCatalogApi.queryEntities?.mockImplementation(request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['metadata.name'] + ) { + return new Promise(() => {}); + } + return mockQueryEntitiesImplementation(request); + }); + render(); + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), + ); + expect(updateFilters).not.toHaveBeenCalledWith({ + user: expect.any(Object), + }); + }); + + it('resets the filter to "all" when entities are loaded, legacy', async () => { + mockCatalogApi.queryEntities?.mockImplementation(async request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['metadata.name'] + ) { + return { items: [], totalItems: 0, pageInfo: {} }; + } + return mockQueryEntitiesImplementation(request); + }); + + render(); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'all', + expect.any(Function), + expect.any(Function), + ), + }), + ); + }); + + it('resets the filter to "all" when entities are loaded', async () => { + mockCatalogApi.queryEntities?.mockImplementation(async request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['metadata.name'] + ) { + return { items: [], totalItems: 0, pageInfo: {} }; + } + return mockQueryEntitiesImplementation(request); + }); + + render(); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: UserOwnersFilter.all(), + }), + ); + }); + }); + + describe(`when there are some owned entities present`, () => { + it('does not reset the filter while entities are loading', async () => { + mockCatalogApi.queryEntities?.mockImplementation(request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['relations.ownedBy'] + ) { + return new Promise(() => {}); + } + return mockQueryEntitiesImplementation(request); + }); + + render( + , + ); /* picker({ loading: true })*/ + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), + ); expect(updateFilters).not.toHaveBeenCalledWith({ user: new UserListFilter( 'all', @@ -366,17 +666,74 @@ describe('', () => { }); }); - it('does not reset the filter when entities are loaded', () => { - render(picker({ loading: false })); + it('does not reset the filter when entities are loaded', async () => { + render(); - expect(updateFilters).toHaveBeenLastCalledWith({ + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), + ); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'owned', + expect.any(Function), + expect.any(Function), + ), + }), + ); + }); + }); + + describe(`when there are some starred entities present`, () => { + it('does not reset the filter while entities are loading', async () => { + mockCatalogApi.queryEntities?.mockImplementation(request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['metadata.name'] + ) { + return new Promise(() => {}); + } + return mockQueryEntitiesImplementation(request); + }); + + render( + , + ); /* picker({ loading: true })*/ + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), + ); + expect(updateFilters).not.toHaveBeenCalledWith({ user: new UserListFilter( - type, + 'all', mockIsOwnedEntity, mockIsStarredEntity, ), }); }); + + it('does not reset the filter when entities are loaded', async () => { + render(); + + await waitFor(() => + expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), + ); + + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'starred', + expect.any(Function), + expect.any(Function), + ), + }), + ); + }); }); }); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index e7c306e0d0..153cdfdb9e 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -32,16 +32,14 @@ import { } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; -import { compact } from 'lodash'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; -import { UserListFilter } from '../../filters'; -import { - useEntityList, - useStarredEntities, - useEntityOwnership, -} from '../../hooks'; +import { UserListFilter, UserOwnersFilter } from '../../filters'; +import { useEntityList, useStarredEntities } from '../../hooks'; import { UserListFilterKind } from '../../types'; -import { reduceEntityFilters } from '../../utils'; +import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; +import { useAllEntitiesCount } from './useAllEntitiesCount'; +import { useStarredEntitiesCount } from './useStarredEntitiesCount'; +import { useIsOwnedEntity } from '../../hooks/useEntityOwnership'; /** @public */ export type CatalogReactUserListPickerClassKey = @@ -122,20 +120,19 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] { export type UserListPickerProps = { initialFilter?: UserListFilterKind; availableFilters?: UserListFilterKind[]; + useServerSideFilters?: boolean; }; /** @public */ export const UserListPicker = (props: UserListPickerProps) => { - const { initialFilter, availableFilters } = props; + const { initialFilter, availableFilters, useServerSideFilters } = props; const classes = useStyles(); const configApi = useApi(configApiRef); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; const { filters, updateFilters, - backendEntities, queryParameters: { kind: kindParameter, user: userParameter }, - loading: loadingBackendEntities, } = useEntityList(); // Remove group items that aren't in availableFilters and exclude @@ -153,21 +150,18 @@ export const UserListPicker = (props: UserListPickerProps) => { })) .filter(({ items }) => !!items.length); - const { isStarredEntity } = useStarredEntities(); - const { isOwnedEntity, loading: loadingEntityOwnership } = - useEntityOwnership(); - - const loading = loadingBackendEntities || loadingEntityOwnership; - - // Static filters; used for generating counts of potentially unselected kinds - const ownedFilter = useMemo( - () => new UserListFilter('owned', isOwnedEntity, isStarredEntity), - [isOwnedEntity, isStarredEntity], - ); - const starredFilter = useMemo( - () => new UserListFilter('starred', isOwnedEntity, isStarredEntity), - [isOwnedEntity, isStarredEntity], - ); + const { + count: ownedEntitiesCount, + loading: loadingOwnedEntities, + filter: ownedEntitiesFilter, + ownershipEntityRefs, + } = useOwnedEntitiesCount(); + const { count: allCount } = useAllEntitiesCount(); + const { + count: starredEntitiesCount, + filter: starredEntitiesFilter, + loading: loadingStarredEntities, + } = useStarredEntitiesCount(); const queryParamUserFilter = useMemo( () => [userParameter].flat()[0], @@ -175,33 +169,19 @@ export const UserListPicker = (props: UserListPickerProps) => { ); const [selectedUserFilter, setSelectedUserFilter] = useState( - queryParamUserFilter ?? initialFilter, + (queryParamUserFilter as UserListFilterKind) ?? initialFilter, ); - // To show proper counts for each section, apply all other frontend filters _except_ the user - // filter that's controlled by this picker. - const entitiesWithoutUserFilter = useMemo( - () => - backendEntities.filter( - reduceEntityFilters( - compact(Object.values({ ...filters, user: undefined })), - ), - ), - [filters, backendEntities], - ); + const filterCounts = useMemo(() => { + return { + all: allCount, + starred: starredEntitiesCount, + owned: ownedEntitiesCount, + }; + }, [starredEntitiesCount, ownedEntitiesCount, allCount]); - const filterCounts = useMemo>( - () => ({ - all: entitiesWithoutUserFilter.length, - starred: entitiesWithoutUserFilter.filter(entity => - starredFilter.filterEntity(entity), - ).length, - owned: entitiesWithoutUserFilter.filter(entity => - ownedFilter.filterEntity(entity), - ).length, - }), - [entitiesWithoutUserFilter, starredFilter, ownedFilter], - ); + const { isStarredEntity } = useStarredEntities(); + const isOwnedEntity = useIsOwnedEntity(ownershipEntityRefs); // Set selected user filter on query parameter updates; this happens at initial page load and from // external updates to the page location. @@ -211,6 +191,8 @@ export const UserListPicker = (props: UserListPickerProps) => { } }, [queryParamUserFilter]); + const loading = loadingOwnedEntities || loadingStarredEntities; + useEffect(() => { if ( !loading && @@ -223,16 +205,46 @@ export const UserListPicker = (props: UserListPickerProps) => { }, [loading, filterCounts, selectedUserFilter, setSelectedUserFilter]); useEffect(() => { - updateFilters({ - user: selectedUserFilter - ? new UserListFilter( - selectedUserFilter as UserListFilterKind, - isOwnedEntity, - isStarredEntity, - ) - : undefined, - }); - }, [selectedUserFilter, isOwnedEntity, isStarredEntity, updateFilters]); + if (!selectedUserFilter) { + return; + } + if (loading) { + return; + } + if (useServerSideFilters) { + const getFilter = () => { + if (selectedUserFilter === 'owned') { + return ownedEntitiesFilter; + } + if (selectedUserFilter === 'starred') { + return starredEntitiesFilter; + } + return UserOwnersFilter.all(); + }; + + updateFilters({ user: getFilter() }); + } else { + // legacy + updateFilters({ + user: selectedUserFilter + ? new UserListFilter( + selectedUserFilter as UserListFilterKind, + isOwnedEntity, + isStarredEntity, + ) + : undefined, + }); + } + }, [ + selectedUserFilter, + starredEntitiesFilter, + ownedEntitiesFilter, + updateFilters, + useServerSideFilters, + isOwnedEntity, + isStarredEntity, + loading, + ]); return ( diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts new file mode 100644 index 0000000000..499762ff87 --- /dev/null +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts @@ -0,0 +1,61 @@ +/* + * 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 { QueryEntitiesInitialRequest } from '@backstage/catalog-client'; +import { useApi } from '@backstage/core-plugin-api'; +import { compact, isEqual } from 'lodash'; +import { useMemo, useRef } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../api'; +import { useEntityList } from '../../hooks'; +import { reduceCatalogFilters } from '../../utils'; + +/** + * TODO(vinzscam): we need to find a better way + * for retrieving this value. One possible way, could be to use + * the /entities endpoint: since this method is paginated, + * it should also return how many items matching the provided filters + * are in the catalog + */ +export function useAllEntitiesCount() { + const catalogApi = useApi(catalogApiRef); + const { filters } = useEntityList(); + + const refRequest = useRef(); + useMemo(() => { + const { user, ...allFilters } = filters; + const compacted = compact(Object.values(allFilters)); + const filter = reduceCatalogFilters(compacted); + const request: QueryEntitiesInitialRequest = { + filter, + limit: 0, + }; + + if (isEqual(request, refRequest.current)) { + return refRequest.current; + } + refRequest.current = request; + + return request; + }, [filters]); + + const { value: count, loading } = useAsync(async () => { + const { totalItems } = await catalogApi.queryEntities(refRequest.current); + + return totalItems; + }, [refRequest.current]); + + return { count, loading }; +} diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts new file mode 100644 index 0000000000..32e367cc07 --- /dev/null +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -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 { QueryEntitiesInitialRequest } from '@backstage/catalog-client'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { compact, intersection, isEqual } from 'lodash'; +import { useMemo, useRef } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../api'; +import { UserOwnersFilter } from '../../filters'; +import { useEntityList } from '../../hooks'; +import { reduceCatalogFilters } from '../../utils'; + +export function useOwnedEntitiesCount() { + const identityApi = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + + const { filters } = useEntityList(); + // Trigger load only on mount + const { value: ownershipEntityRefs, loading: loadingEntityRefs } = useAsync( + async () => (await identityApi.getBackstageIdentity()).ownershipEntityRefs, + + [], + ); + + const refRequest = useRef(); + + useMemo(async () => { + const compacted = compact(Object.values(filters)); + const allFilter = reduceCatalogFilters(compacted); + const { ['metadata.name']: metadata, ...filter } = allFilter; + + const facet = 'relations.ownedBy'; + + const ownedByFilter = Array.isArray(filter[facet]) + ? (filter[facet] as string[]) + : []; + + const commonOwnedBy = intersection(ownedByFilter, ownershipEntityRefs); + + const ownedBy = + ownedByFilter.length > 0 ? ownedByFilter : ownershipEntityRefs; + if (ownedByFilter.length > 0 && commonOwnedBy.length === 0) { + // don't send any request if another filter sets + // totally different values for relations.ownedBy filter. + // TODO(vinzscam): check conflicts between UserOwnersFilter and EntityOwnerFilter. + // both set filters on the same relations.ownedBy key, so the conflicts need + // to be addressed properly. + refRequest.current = undefined; + return null; + } + const request: QueryEntitiesInitialRequest = { + filter: { + ...filter, + 'relations.ownedBy': ownedBy ?? [], + }, + limit: 0, + }; + + if (isEqual(request, refRequest.current)) { + return refRequest.current; + } + + refRequest.current = request; + + return request; + }, [filters, ownershipEntityRefs]); + + const { value: count, loading: loadingEntityOwnership } = + useAsync(async () => { + if (!ownershipEntityRefs?.length) { + return 0; + } + if (!refRequest.current) { + return 0; + } + const { totalItems } = await catalogApi.queryEntities(refRequest.current); + + return totalItems; + }, [refRequest.current]); + + const loading = loadingEntityRefs || loadingEntityOwnership; + const filter = useMemo( + () => UserOwnersFilter.owned(ownershipEntityRefs ?? []), + [ownershipEntityRefs], + ); + + return { + count, + loading, + filter, + ownershipEntityRefs, + }; +} diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts new file mode 100644 index 0000000000..ea7423efe8 --- /dev/null +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts @@ -0,0 +1,80 @@ +/* + * 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 { QueryEntitiesInitialRequest } from '@backstage/catalog-client'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; +import { compact, isEqual } from 'lodash'; +import { useMemo, useRef } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../api'; +import { UserOwnersFilter } from '../../filters'; +import { useEntityList, useStarredEntities } from '../../hooks'; +import { reduceCatalogFilters } from '../../utils'; + +export function useStarredEntitiesCount() { + const catalogApi = useApi(catalogApiRef); + const { filters } = useEntityList(); + const { starredEntities } = useStarredEntities(); + + const refRequest = useRef(); + useMemo(async () => { + const { user, ...allFilters } = filters; + const compacted = compact(Object.values(allFilters)); + const filter = reduceCatalogFilters(compacted); + + const facet = 'metadata.name'; + + const request: QueryEntitiesInitialRequest = { + filter: { + ...filter, + [facet]: Array.from(starredEntities).map(e => parseEntityRef(e).name), + }, + limit: 1000, + }; + if (isEqual(request, refRequest.current)) { + return refRequest.current; + } + refRequest.current = request; + + return request; + }, [filters, starredEntities]); + + const { value: count, loading } = useAsync(async () => { + if (!starredEntities.size) { + return 0; + } + + const response = await catalogApi.queryEntities(refRequest.current); + + return response.items + .map(e => + stringifyEntityRef({ + kind: e.kind, + namespace: e.metadata.namespace, + name: e.metadata.name, + }), + ) + .filter(e => starredEntities.has(e)).length; + }, [refRequest.current, starredEntities]); + + const filter = useMemo( + () => UserOwnersFilter.starred(Array.from(starredEntities)), + [starredEntities], + ); + + return { count, loading, filter }; +} From eb81d1673d35f9de06d60ae0cb57cbf422bbb140 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 26 May 2023 14:04:07 +0200 Subject: [PATCH 22/70] catalog-react: rename filter to EntityUserListFilter Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/UserListPicker.test.tsx | 14 +++++++------- .../components/UserListPicker/UserListPicker.tsx | 4 ++-- .../UserListPicker/useOwnedEntitiesCount.ts | 4 ++-- .../UserListPicker/useStarredEntitiesCount.ts | 4 ++-- plugins/catalog-react/src/filters.ts | 10 +++++----- .../src/hooks/useEntityListProvider.tsx | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 1315c95061..20dedf23a6 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -22,7 +22,7 @@ import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityTagFilter, UserListFilter, - UserOwnersFilter, + EntityUserListFilter, } from '../../filters'; import { CatalogApi, @@ -286,7 +286,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: UserOwnersFilter.owned(ownershipEntityRefs), + user: EntityUserListFilter.owned(ownershipEntityRefs), }), ); @@ -343,7 +343,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: UserOwnersFilter.starred([ + user: EntityUserListFilter.starred([ 'component:default/e-1', 'component:default/e-2', ]), @@ -414,7 +414,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: UserOwnersFilter.all(), + user: EntityUserListFilter.all(), }), ); @@ -431,7 +431,7 @@ describe('', () => { , ); expect(updateFilters).toHaveBeenLastCalledWith({ - user: UserOwnersFilter.owned(ownershipEntityRefs), + user: EntityUserListFilter.owned(ownershipEntityRefs), }); }); @@ -536,7 +536,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: UserOwnersFilter.all(), + user: EntityUserListFilter.all(), }), ); }); @@ -628,7 +628,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: UserOwnersFilter.all(), + user: EntityUserListFilter.all(), }), ); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 153cdfdb9e..dff4f995ae 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -33,7 +33,7 @@ import { import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; -import { UserListFilter, UserOwnersFilter } from '../../filters'; +import { UserListFilter, EntityUserListFilter } from '../../filters'; import { useEntityList, useStarredEntities } from '../../hooks'; import { UserListFilterKind } from '../../types'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; @@ -219,7 +219,7 @@ export const UserListPicker = (props: UserListPickerProps) => { if (selectedUserFilter === 'starred') { return starredEntitiesFilter; } - return UserOwnersFilter.all(); + return EntityUserListFilter.all(); }; updateFilters({ user: getFilter() }); diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts index 32e367cc07..f7fb9d4516 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -20,7 +20,7 @@ import { compact, intersection, isEqual } from 'lodash'; import { useMemo, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; -import { UserOwnersFilter } from '../../filters'; +import { EntityUserListFilter } from '../../filters'; import { useEntityList } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; @@ -94,7 +94,7 @@ export function useOwnedEntitiesCount() { const loading = loadingEntityRefs || loadingEntityOwnership; const filter = useMemo( - () => UserOwnersFilter.owned(ownershipEntityRefs ?? []), + () => EntityUserListFilter.owned(ownershipEntityRefs ?? []), [ownershipEntityRefs], ); diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts index ea7423efe8..aff5066a74 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts @@ -21,7 +21,7 @@ import { compact, isEqual } from 'lodash'; import { useMemo, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; -import { UserOwnersFilter } from '../../filters'; +import { EntityUserListFilter } from '../../filters'; import { useEntityList, useStarredEntities } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; @@ -72,7 +72,7 @@ export function useStarredEntitiesCount() { }, [refRequest.current, starredEntities]); const filter = useMemo( - () => UserOwnersFilter.starred(Array.from(starredEntities)), + () => EntityUserListFilter.starred(Array.from(starredEntities)), [starredEntities], ); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index a5a39f9b02..d0ab093e3c 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -203,22 +203,22 @@ export class EntityNamespaceFilter implements EntityFilter { /** * @public */ -export class UserOwnersFilter implements EntityFilter { +export class EntityUserListFilter implements EntityFilter { private constructor( readonly value: UserListFilterKind, readonly refs?: string[], ) {} static owned(ownershipEntityRefs: string[]) { - return new UserOwnersFilter('owned', ownershipEntityRefs); + return new EntityUserListFilter('owned', ownershipEntityRefs); } static all() { - return new UserOwnersFilter('all'); + return new EntityUserListFilter('all'); } static starred(starredEntityRefs: string[]) { - return new UserOwnersFilter('starred', starredEntityRefs); + return new EntityUserListFilter('starred', starredEntityRefs); } getCatalogFilters(): Record { @@ -247,7 +247,7 @@ export class UserOwnersFilter implements EntityFilter { /** * Filters entities based on whatever the user has starred or owns them. - * @deprecated use UserOwnersFilter + * @deprecated use EntityUserListFilter * @public */ export class UserListFilter implements EntityFilter { diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index b7ee8a94ed..e2a8cfc657 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -41,7 +41,7 @@ import { EntityTypeFilter, UserListFilter, EntityNamespaceFilter, - UserOwnersFilter, + EntityUserListFilter, } from '../filters'; import { EntityFilter } from '../types'; import { reduceBackendCatalogFilters, reduceEntityFilters } from '../utils'; @@ -51,7 +51,7 @@ import { useApi } from '@backstage/core-plugin-api'; export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; - user?: UserListFilter | UserOwnersFilter; + user?: UserListFilter | EntityUserListFilter; owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; From 78b2610717546e406def7f2a55c3c385b7f0b656 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 26 May 2023 14:14:48 +0200 Subject: [PATCH 23/70] catalog-react: remove UserListFilter usage Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/api-report.md | 43 ++-- .../UserListPicker/UserListPicker.test.tsx | 215 +----------------- .../UserListPicker/UserListPicker.tsx | 50 ++-- plugins/catalog-react/src/filters.ts | 9 + .../src/hooks/useEntityListProvider.test.tsx | 23 +- .../src/hooks/useEntityOwnership.ts | 14 +- 6 files changed, 73 insertions(+), 281 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 3faa5524c9..e73441f7dc 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -142,7 +142,7 @@ export const columnFactories: Readonly<{ export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; - user?: UserListFilter | UserOwnersFilter; + user?: UserListFilter | EntityUserListFilter; owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; @@ -507,6 +507,26 @@ export interface EntityTypePickerProps { initialFilter?: string; } +// @public (undocumented) +export class EntityUserListFilter implements EntityFilter { + // (undocumented) + static all(): EntityUserListFilter; + // (undocumented) + filterEntity(entity: Entity): boolean; + // (undocumented) + getCatalogFilters(): Record; + // (undocumented) + static owned(ownershipEntityRefs: string[]): EntityUserListFilter; + // (undocumented) + readonly refs?: string[] | undefined; + // (undocumented) + static starred(starredEntityRefs: string[]): EntityUserListFilter; + // (undocumented) + toQueryValue(): string; + // (undocumented) + readonly value: UserListFilterKind; +} + // @public export const FavoriteEntity: ( props: FavoriteEntityProps, @@ -661,29 +681,8 @@ export const UserListPicker: ( export type UserListPickerProps = { initialFilter?: UserListFilterKind; availableFilters?: UserListFilterKind[]; - useServerSideFilters?: boolean; }; -// @public (undocumented) -export class UserOwnersFilter implements EntityFilter { - // (undocumented) - static all(): UserOwnersFilter; - // (undocumented) - filterEntity(entity: Entity): boolean; - // (undocumented) - getCatalogFilters(): Record; - // (undocumented) - static owned(ownershipEntityRefs: string[]): UserOwnersFilter; - // (undocumented) - readonly refs?: string[] | undefined; - // (undocumented) - static starred(starredEntityRefs: string[]): UserOwnersFilter; - // (undocumented) - toQueryValue(): string; - // (undocumented) - readonly value: UserListFilterKind; -} - // @public (undocumented) export function useStarredEntities(): { starredEntities: Set; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 20dedf23a6..ed6c43d12e 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -16,14 +16,10 @@ import React from 'react'; import { fireEvent, render, waitFor, screen } from '@testing-library/react'; -import { Entity, UserEntity } from '@backstage/catalog-model'; +import { UserEntity } from '@backstage/catalog-model'; import { UserListPicker, UserListPickerProps } from './UserListPicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { - EntityTagFilter, - UserListFilter, - EntityUserListFilter, -} from '../../filters'; +import { EntityTagFilter, EntityUserListFilter } from '../../filters'; import { CatalogApi, QueryEntitiesInitialRequest, @@ -75,14 +71,6 @@ const apis = TestApiRegistry.from( [starredEntitiesApiRef, mockStarredEntitiesApi], ); -const mockIsOwnedEntity = jest.fn( - (entity: Entity) => entity.metadata.name === 'component-1', -); - -const mockIsStarredEntity = jest.fn( - (entity: Entity) => entity.metadata.name === 'component-3', -); - const ownershipEntityRefs = ['user:default/testuser']; describe('', () => { const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] = @@ -233,44 +221,6 @@ describe('', () => { }); }); - it('respects the query parameter filter value, legacy', async () => { - const updateFilters = jest.fn(); - const queryParameters = { user: 'owned' }; - render( - - - - - , - ); - - await waitFor(() => - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'owned', - expect.any(Function), - expect.any(Function), - ), - }), - ); - expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: {}, - limit: 0, - }); - expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: { 'metadata.name': ['e-1', 'e-2'] }, - limit: 1000, - }); - expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: { - 'relations.ownedBy': ['user:default/testuser'], - }, - limit: 0, - }); - }); - it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { user: 'owned' }; @@ -279,7 +229,7 @@ describe('', () => { - + , ); @@ -306,35 +256,12 @@ describe('', () => { }); }); - it('updates user filter when a menuitem is selected, legacy', async () => { - const updateFilters = jest.fn(); - render( - - - - - , - ); - - fireEvent.click(screen.getByText('Starred')); - - await waitFor(() => - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'starred', - expect.any(Function), - expect.any(Function), - ), - }), - ); - }); - it('updates user filter when a menuitem is selected', async () => { const updateFilters = jest.fn(); render( - + , ); @@ -351,52 +278,6 @@ describe('', () => { ); }); - it('responds to external queryParameters changes, legacy', async () => { - const updateFilters = jest.fn(); - const rendered = render( - - - - - , - ); - - await waitFor(() => - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'all', - expect.any(Function), - expect.any(Function), - ), - }), - ); - - rendered.rerender( - - - - - , - ); - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'owned', - expect.any(Function), - expect.any(Function), - ), - }); - }); - it('responds to external queryParameters changes', async () => { const updateFilters = jest.fn(); const rendered = render( @@ -407,7 +288,7 @@ describe('', () => { queryParameters: { user: ['all'] }, }} > - + , ); @@ -426,7 +307,7 @@ describe('', () => { queryParameters: { user: ['owned'] }, }} > - + , ); @@ -489,34 +370,6 @@ describe('', () => { }); }); - it('resets the filter to "all" when entities are loaded, legacy', async () => { - mockCatalogApi.queryEntities?.mockImplementation(async request => { - if ( - ( - (request as QueryEntitiesInitialRequest).filter as Record< - string, - string - > - )['relations.ownedBy'] - ) { - return { items: [], totalItems: 0, pageInfo: {} }; - } - return mockQueryEntitiesImplementation(request); - }); - - render(); - - await waitFor(() => - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'all', - expect.any(Function), - expect.any(Function), - ), - }), - ); - }); - it('resets the filter to "all" when entities are loaded', async () => { mockCatalogApi.queryEntities?.mockImplementation(async request => { if ( @@ -532,7 +385,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + render(); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -581,34 +434,6 @@ describe('', () => { }); }); - it('resets the filter to "all" when entities are loaded, legacy', async () => { - mockCatalogApi.queryEntities?.mockImplementation(async request => { - if ( - ( - (request as QueryEntitiesInitialRequest).filter as Record< - string, - string - > - )['metadata.name'] - ) { - return { items: [], totalItems: 0, pageInfo: {} }; - } - return mockQueryEntitiesImplementation(request); - }); - - render(); - - await waitFor(() => - expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'all', - expect.any(Function), - expect.any(Function), - ), - }), - ); - }); - it('resets the filter to "all" when entities are loaded', async () => { mockCatalogApi.queryEntities?.mockImplementation(async request => { if ( @@ -624,7 +449,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + render(); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -658,11 +483,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), ); expect(updateFilters).not.toHaveBeenCalledWith({ - user: new UserListFilter( - 'all', - mockIsOwnedEntity, - mockIsStarredEntity, - ), + user: EntityUserListFilter.all(), }); }); @@ -675,11 +496,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'owned', - expect.any(Function), - expect.any(Function), - ), + user: EntityUserListFilter.owned(expect.any(Array)), }), ); }); @@ -709,11 +526,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), ); expect(updateFilters).not.toHaveBeenCalledWith({ - user: new UserListFilter( - 'all', - mockIsOwnedEntity, - mockIsStarredEntity, - ), + user: EntityUserListFilter.all(), }); }); @@ -726,11 +539,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: new UserListFilter( - 'starred', - expect.any(Function), - expect.any(Function), - ), + user: EntityUserListFilter.starred(expect.any(Array)), }), ); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index dff4f995ae..b0a86b1322 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -33,13 +33,12 @@ import { import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; -import { UserListFilter, EntityUserListFilter } from '../../filters'; -import { useEntityList, useStarredEntities } from '../../hooks'; +import { EntityUserListFilter } from '../../filters'; +import { useEntityList } from '../../hooks'; import { UserListFilterKind } from '../../types'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; import { useAllEntitiesCount } from './useAllEntitiesCount'; import { useStarredEntitiesCount } from './useStarredEntitiesCount'; -import { useIsOwnedEntity } from '../../hooks/useEntityOwnership'; /** @public */ export type CatalogReactUserListPickerClassKey = @@ -120,12 +119,11 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] { export type UserListPickerProps = { initialFilter?: UserListFilterKind; availableFilters?: UserListFilterKind[]; - useServerSideFilters?: boolean; }; /** @public */ export const UserListPicker = (props: UserListPickerProps) => { - const { initialFilter, availableFilters, useServerSideFilters } = props; + const { initialFilter, availableFilters } = props; const classes = useStyles(); const configApi = useApi(configApiRef); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; @@ -154,7 +152,6 @@ export const UserListPicker = (props: UserListPickerProps) => { count: ownedEntitiesCount, loading: loadingOwnedEntities, filter: ownedEntitiesFilter, - ownershipEntityRefs, } = useOwnedEntitiesCount(); const { count: allCount } = useAllEntitiesCount(); const { @@ -180,9 +177,6 @@ export const UserListPicker = (props: UserListPickerProps) => { }; }, [starredEntitiesCount, ownedEntitiesCount, allCount]); - const { isStarredEntity } = useStarredEntities(); - const isOwnedEntity = useIsOwnedEntity(ownershipEntityRefs); - // Set selected user filter on query parameter updates; this happens at initial page load and from // external updates to the page location. useEffect(() => { @@ -211,38 +205,24 @@ export const UserListPicker = (props: UserListPickerProps) => { if (loading) { return; } - if (useServerSideFilters) { - const getFilter = () => { - if (selectedUserFilter === 'owned') { - return ownedEntitiesFilter; - } - if (selectedUserFilter === 'starred') { - return starredEntitiesFilter; - } - return EntityUserListFilter.all(); - }; - updateFilters({ user: getFilter() }); - } else { - // legacy - updateFilters({ - user: selectedUserFilter - ? new UserListFilter( - selectedUserFilter as UserListFilterKind, - isOwnedEntity, - isStarredEntity, - ) - : undefined, - }); - } + const getFilter = () => { + if (selectedUserFilter === 'owned') { + return ownedEntitiesFilter; + } + if (selectedUserFilter === 'starred') { + return starredEntitiesFilter; + } + return EntityUserListFilter.all(); + }; + + updateFilters({ user: getFilter() }); }, [ selectedUserFilter, starredEntitiesFilter, ownedEntitiesFilter, updateFilters, - useServerSideFilters, - isOwnedEntity, - isStarredEntity, + loading, ]); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index d0ab093e3c..bafaae4fb2 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -237,6 +237,15 @@ export class EntityUserListFilter implements EntityFilter { if (this.value === 'starred') { return this.refs?.includes(stringifyEntityRef(entity)) ?? true; } + if (this.value === 'owned') { + return ( + this.refs?.some(v => + getEntityRelations(entity, RELATION_OWNED_BY).some( + o => stringifyEntityRef(o) === v, + ), + ) ?? false + ); + } return true; } diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index d77655ebcc..a4401e3441 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -32,7 +32,11 @@ import { MemoryRouter } from 'react-router-dom'; import { catalogApiRef } from '../api'; import { starredEntitiesApiRef, MockStarredEntitiesApi } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; -import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../filters'; +import { + EntityKindFilter, + EntityTypeFilter, + EntityUserListFilter, +} from '../filters'; import { UserListFilterKind } from '../types'; import { EntityListProvider, useEntityList } from './useEntityListProvider'; @@ -62,11 +66,14 @@ const entities: Entity[] = [ const mockConfigApi = { getOptionalString: () => '', } as Partial; + +const ownershipEntityRefs = ['user:default/guest']; + const mockIdentityApi: Partial = { getBackstageIdentity: async () => ({ type: 'user', userEntityRef: 'user:default/guest', - ownershipEntityRefs: [], + ownershipEntityRefs, }), getCredentials: async () => ({ token: undefined }), }; @@ -148,11 +155,7 @@ describe('', () => { act(() => result.current.updateFilters({ - user: new UserListFilter( - 'owned', - entity => entity.metadata.name === 'component-1', - () => true, - ), + user: EntityUserListFilter.owned(ownershipEntityRefs), }), ); @@ -193,11 +196,7 @@ describe('', () => { act(() => result.current.updateFilters({ - user: new UserListFilter( - 'owned', - entity => entity.metadata.name === 'component-1', - () => true, - ), + user: EntityUserListFilter.owned(ownershipEntityRefs), }), ); diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 1f1e200634..9c86d6e01a 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -46,15 +46,10 @@ export function useEntityOwnership(): { return ownershipEntityRefs; }, []); - const isOwnedEntity = useIsOwnedEntity(refs); - - return useMemo(() => ({ loading, isOwnedEntity }), [loading, isOwnedEntity]); -} - -export function useIsOwnedEntity(refs?: string[]) { - return useMemo(() => { + const isOwnedEntity = useMemo(() => { const myOwnerRefs = new Set(refs ?? []); - const isOwnedEntity = (entity: Entity) => { + + return (entity: Entity) => { const entityOwnerRefs = getEntityRelations(entity, RELATION_OWNED_BY).map( stringifyEntityRef, ); @@ -65,6 +60,7 @@ export function useIsOwnedEntity(refs?: string[]) { } return false; }; - return isOwnedEntity; }, [refs]); + + return { loading, isOwnedEntity }; } From 971a108ab168be535f27aab121603b7d7beea2f3 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 26 May 2023 14:19:25 +0200 Subject: [PATCH 24/70] catalog-react: add clarifying comment to EntityUserListFilter Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/filters.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index bafaae4fb2..5879d91591 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -237,6 +237,10 @@ export class EntityUserListFilter implements EntityFilter { if (this.value === 'starred') { return this.refs?.includes(stringifyEntityRef(entity)) ?? true; } + // used only for retro-compatibility with the old + // non paginated table. This is supposed to return always true + // for paginated owned entities, since the filters are applied + // server side. if (this.value === 'owned') { return ( this.refs?.some(v => From d46acd694893c2291ebbe932efc1e0afcda69a04 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 26 May 2023 23:29:13 +0200 Subject: [PATCH 25/70] catalog-react: improve memo readability Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/useAllEntitiesCount.ts | 26 ++++++--------- .../UserListPicker/useOwnedEntitiesCount.ts | 32 ++++++++----------- .../UserListPicker/useStarredEntitiesCount.ts | 18 +++++------ 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts index 499762ff87..13217c6800 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts @@ -22,40 +22,32 @@ import { catalogApiRef } from '../../api'; import { useEntityList } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; -/** - * TODO(vinzscam): we need to find a better way - * for retrieving this value. One possible way, could be to use - * the /entities endpoint: since this method is paginated, - * it should also return how many items matching the provided filters - * are in the catalog - */ export function useAllEntitiesCount() { const catalogApi = useApi(catalogApiRef); const { filters } = useEntityList(); - const refRequest = useRef(); - useMemo(() => { + const prevRequest = useRef(); + const request = useMemo(() => { const { user, ...allFilters } = filters; const compacted = compact(Object.values(allFilters)); const filter = reduceCatalogFilters(compacted); - const request: QueryEntitiesInitialRequest = { + const newRequest: QueryEntitiesInitialRequest = { filter, limit: 0, }; - if (isEqual(request, refRequest.current)) { - return refRequest.current; + if (isEqual(newRequest, prevRequest.current)) { + return prevRequest.current; } - refRequest.current = request; - - return request; + prevRequest.current = newRequest; + return newRequest; }, [filters]); const { value: count, loading } = useAsync(async () => { - const { totalItems } = await catalogApi.queryEntities(refRequest.current); + const { totalItems } = await catalogApi.queryEntities(request); return totalItems; - }, [refRequest.current]); + }, [request]); return { count, loading }; } diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts index f7fb9d4516..f2ff985c09 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -32,13 +32,12 @@ export function useOwnedEntitiesCount() { // Trigger load only on mount const { value: ownershipEntityRefs, loading: loadingEntityRefs } = useAsync( async () => (await identityApi.getBackstageIdentity()).ownershipEntityRefs, - [], ); - const refRequest = useRef(); + const prevRequest = useRef(); - useMemo(async () => { + const request = useMemo(() => { const compacted = compact(Object.values(filters)); const allFilter = reduceCatalogFilters(compacted); const { ['metadata.name']: metadata, ...filter } = allFilter; @@ -54,15 +53,12 @@ export function useOwnedEntitiesCount() { const ownedBy = ownedByFilter.length > 0 ? ownedByFilter : ownershipEntityRefs; if (ownedByFilter.length > 0 && commonOwnedBy.length === 0) { - // don't send any request if another filter sets - // totally different values for relations.ownedBy filter. - // TODO(vinzscam): check conflicts between UserOwnersFilter and EntityOwnerFilter. - // both set filters on the same relations.ownedBy key, so the conflicts need - // to be addressed properly. - refRequest.current = undefined; - return null; + // detect whether another filter sets values that will produce + // empty results in order to avoid sending an additional request. + prevRequest.current = undefined; + return undefined; } - const request: QueryEntitiesInitialRequest = { + const newRequest: QueryEntitiesInitialRequest = { filter: { ...filter, 'relations.ownedBy': ownedBy ?? [], @@ -70,13 +66,13 @@ export function useOwnedEntitiesCount() { limit: 0, }; - if (isEqual(request, refRequest.current)) { - return refRequest.current; + if (isEqual(newRequest, prevRequest.current)) { + return prevRequest.current; } - refRequest.current = request; + prevRequest.current = newRequest; - return request; + return newRequest; }, [filters, ownershipEntityRefs]); const { value: count, loading: loadingEntityOwnership } = @@ -84,13 +80,13 @@ export function useOwnedEntitiesCount() { if (!ownershipEntityRefs?.length) { return 0; } - if (!refRequest.current) { + if (!request) { return 0; } - const { totalItems } = await catalogApi.queryEntities(refRequest.current); + const { totalItems } = await catalogApi.queryEntities(request); return totalItems; - }, [refRequest.current]); + }, [request]); const loading = loadingEntityRefs || loadingEntityOwnership; const filter = useMemo( diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts index aff5066a74..93adad0489 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts @@ -30,27 +30,27 @@ export function useStarredEntitiesCount() { const { filters } = useEntityList(); const { starredEntities } = useStarredEntities(); - const refRequest = useRef(); - useMemo(async () => { + const prevRequest = useRef(); + const request = useMemo(() => { const { user, ...allFilters } = filters; const compacted = compact(Object.values(allFilters)); const filter = reduceCatalogFilters(compacted); const facet = 'metadata.name'; - const request: QueryEntitiesInitialRequest = { + const newRequest: QueryEntitiesInitialRequest = { filter: { ...filter, [facet]: Array.from(starredEntities).map(e => parseEntityRef(e).name), }, limit: 1000, }; - if (isEqual(request, refRequest.current)) { - return refRequest.current; + if (isEqual(newRequest, prevRequest.current)) { + return prevRequest.current; } - refRequest.current = request; + prevRequest.current = newRequest; - return request; + return newRequest; }, [filters, starredEntities]); const { value: count, loading } = useAsync(async () => { @@ -58,7 +58,7 @@ export function useStarredEntitiesCount() { return 0; } - const response = await catalogApi.queryEntities(refRequest.current); + const response = await catalogApi.queryEntities(request); return response.items .map(e => @@ -69,7 +69,7 @@ export function useStarredEntitiesCount() { }), ) .filter(e => starredEntities.has(e)).length; - }, [refRequest.current, starredEntities]); + }, [request, starredEntities]); const filter = useMemo( () => EntityUserListFilter.starred(Array.from(starredEntities)), From 167d08dca03e6387b8585bae8682a910f167540f Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 26 May 2023 23:41:36 +0200 Subject: [PATCH 26/70] catalog-react: clarify comment Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/filters.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 5879d91591..d6bd8eee3f 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -237,10 +237,9 @@ export class EntityUserListFilter implements EntityFilter { if (this.value === 'starred') { return this.refs?.includes(stringifyEntityRef(entity)) ?? true; } - // used only for retro-compatibility with the old - // non paginated table. This is supposed to return always true - // for paginated owned entities, since the filters are applied - // server side. + // used only for retro-compatibility with non paginated data. + // This is supposed to return always true for paginated + // owned entities, since the filters are applied server side. if (this.value === 'owned') { return ( this.refs?.some(v => From a996c54f68968712e95b08f09c177c73f1af5ec2 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Jun 2023 19:33:44 +0200 Subject: [PATCH 27/70] catalog-react: fix EntityNamespaceFilter Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/filters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index d6bd8eee3f..b94b538802 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -189,7 +189,7 @@ export class EntityNamespaceFilter implements EntityFilter { constructor(readonly values: string[]) {} getCatalogFilters(): Record { - return { 'spec.lifecycle': this.values }; + return { 'metadata.namespace': this.values }; } filterEntity(entity: Entity): boolean { return this.values.some(v => entity.metadata.namespace === v); From 633358a05bef97141564407fe10fe732998456fb Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Jun 2023 19:34:51 +0200 Subject: [PATCH 28/70] catalog-react: improve comment Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/useOwnedEntitiesCount.ts | 3 ++- .../catalog-react/src/hooks/useEntityOwnership.ts | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts index f2ff985c09..5eac3ced60 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -29,9 +29,10 @@ export function useOwnedEntitiesCount() { const catalogApi = useApi(catalogApiRef); const { filters } = useEntityList(); - // Trigger load only on mount + const { value: ownershipEntityRefs, loading: loadingEntityRefs } = useAsync( async () => (await identityApi.getBackstageIdentity()).ownershipEntityRefs, + // load only on mount [], ); diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 9c86d6e01a..fdd73434ec 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -41,10 +41,14 @@ export function useEntityOwnership(): { const identityApi = useApi(identityApiRef); // Trigger load only on mount - const { loading, value: refs } = useAsync(async () => { - const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); - return ownershipEntityRefs; - }, []); + const { loading, value: refs } = useAsync( + async () => { + const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); + return ownershipEntityRefs; + }, + // load only on mount + [], + ); const isOwnedEntity = useMemo(() => { const myOwnerRefs = new Set(refs ?? []); From ead3cb82f64ef1601bf14e57c8af41e669c96643 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 20 Jun 2023 13:57:23 +0200 Subject: [PATCH 29/70] catalog-react: do not send request when filters are loading Signed-off-by: Vincenzo Scamporlino --- .../src/components/UserListPicker/useAllEntitiesCount.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts index 13217c6800..ba971b088d 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.ts @@ -36,6 +36,11 @@ export function useAllEntitiesCount() { limit: 0, }; + if (Object.keys(filter).length === 0) { + prevRequest.current = undefined; + return prevRequest.current; + } + if (isEqual(newRequest, prevRequest.current)) { return prevRequest.current; } @@ -44,6 +49,9 @@ export function useAllEntitiesCount() { }, [filters]); const { value: count, loading } = useAsync(async () => { + if (request === undefined) { + return 0; + } const { totalItems } = await catalogApi.queryEntities(request); return totalItems; From ecf4b77d50bedc1724678447f2c10a712590eb89 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 20 Jun 2023 13:58:05 +0200 Subject: [PATCH 30/70] catalog-react: add useAllEntitiesCount tests Signed-off-by: Vincenzo Scamporlino --- .../useAllEntitiesCount.test.tsx | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx new file mode 100644 index 0000000000..61c2c6eb0a --- /dev/null +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx @@ -0,0 +1,106 @@ +/* + * 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, { PropsWithChildren } from 'react'; +import { CatalogApi } from '@backstage/catalog-client'; +import { useAllEntitiesCount } from './useAllEntitiesCount'; +import { waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { EntityListProvider, useEntityList } from '../../hooks'; +import { catalogApiRef } from '../../api'; +import { ApiRef } from '@backstage/core-plugin-api'; +import { MemoryRouter } from 'react-router-dom'; +import { EntityOwnerFilter } from '../../filters'; +import { useMountEffect } from '@react-hookz/web'; + +const mockQueryEntities: jest.MockedFn = jest.fn(); +const mockCatalogApi: jest.Mocked> = { + queryEntities: mockQueryEntities, +}; + +jest.mock('@backstage/core-plugin-api', () => { + const actual = jest.requireActual('@backstage/core-plugin-api'); + return { + ...actual, + useApi: (ref: ApiRef) => + ref === catalogApiRef ? mockCatalogApi : actual.useApi(ref), + }; +}); + +describe('useAllEntitiesCount', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the count', async () => { + mockQueryEntities.mockResolvedValue({ + items: [], + totalItems: 10, + pageInfo: {}, + }); + + function WrapFilters(props: PropsWithChildren<{}>) { + const { updateFilters } = useEntityList(); + + useMountEffect(() => { + updateFilters({ + owners: new EntityOwnerFilter(['user:default/owner']), + }); + }); + return <>{props.children}; + } + + const { result } = renderHook(() => useAllEntitiesCount(), { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + await waitFor(() => + expect(mockQueryEntities).toHaveBeenCalledWith({ + filter: { + 'relations.ownedBy': ['user:default/owner'], + }, + limit: 0, + }), + ); + expect(result.current).toEqual({ count: 10, loading: false }); + }); + + it(`shouldn't invoke the endpoint at startup, when filters are missing`, async () => { + mockQueryEntities.mockResolvedValue({ + items: [], + totalItems: 10, + pageInfo: {}, + }); + + const { result } = renderHook(() => useAllEntitiesCount(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await expect( + waitFor(() => expect(mockQueryEntities).toHaveBeenCalled()), + ).rejects.toThrow(); + expect(result.current).toEqual({ count: 0, loading: false }); + }); +}); From 84822daea6914e82967a03c05ae7b8ab5c339630 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 20 Jun 2023 13:59:41 +0200 Subject: [PATCH 31/70] catalog-react: send proper filters Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/useOwnedEntitiesCount.ts | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts index 5eac3ced60..605c006f44 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -20,7 +20,7 @@ import { compact, intersection, isEqual } from 'lodash'; import { useMemo, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; -import { EntityUserListFilter } from '../../filters'; +import { EntityOwnerFilter, EntityUserListFilter } from '../../filters'; import { useEntityList } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; @@ -39,30 +39,25 @@ export function useOwnedEntitiesCount() { const prevRequest = useRef(); const request = useMemo(() => { - const compacted = compact(Object.values(filters)); + const { user, owners, ...allFilters } = filters; + const compacted = compact(Object.values(allFilters)); const allFilter = reduceCatalogFilters(compacted); const { ['metadata.name']: metadata, ...filter } = allFilter; - const facet = 'relations.ownedBy'; + const countFilter = getOwnedCountClaims(owners, ownershipEntityRefs); - const ownedByFilter = Array.isArray(filter[facet]) - ? (filter[facet] as string[]) - : []; - - const commonOwnedBy = intersection(ownedByFilter, ownershipEntityRefs); - - const ownedBy = - ownedByFilter.length > 0 ? ownedByFilter : ownershipEntityRefs; - if (ownedByFilter.length > 0 && commonOwnedBy.length === 0) { - // detect whether another filter sets values that will produce - // empty results in order to avoid sending an additional request. + if ( + ownershipEntityRefs?.length === 0 || + countFilter === undefined || + Object.keys(filter).length === 0 + ) { prevRequest.current = undefined; return undefined; } const newRequest: QueryEntitiesInitialRequest = { filter: { ...filter, - 'relations.ownedBy': ownedBy ?? [], + 'relations.ownedBy': countFilter, }, limit: 0, }; @@ -78,14 +73,10 @@ export function useOwnedEntitiesCount() { const { value: count, loading: loadingEntityOwnership } = useAsync(async () => { - if (!ownershipEntityRefs?.length) { - return 0; - } if (!request) { return 0; } const { totalItems } = await catalogApi.queryEntities(request); - return totalItems; }, [request]); @@ -102,3 +93,21 @@ export function useOwnedEntitiesCount() { ownershipEntityRefs, }; } + +function getOwnedCountClaims( + owners: EntityOwnerFilter | undefined, + ownershipEntityRefs: string[] | undefined, +) { + if (ownershipEntityRefs === undefined) { + return undefined; + } + const ownersRefs = owners?.values ?? []; + if (ownersRefs.length) { + const commonOwnedBy = intersection(ownersRefs, ownershipEntityRefs); + if (commonOwnedBy.length === 0) { + return undefined; + } + return commonOwnedBy; + } + return ownershipEntityRefs; +} From 7709c264cabc4c2fc8b0c9d609c02c4cc8abeacf Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 20 Jun 2023 15:22:50 +0200 Subject: [PATCH 32/70] catalog-react: add useOwnedEntitiesCount tests Signed-off-by: Vincenzo Scamporlino --- .../useOwnedEntitiesCount.test.tsx | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx new file mode 100644 index 0000000000..6a79e63c8f --- /dev/null +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -0,0 +1,235 @@ +/* + * 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, { PropsWithChildren } from 'react'; +import { CatalogApi } from '@backstage/catalog-client'; +import { renderHook } from '@testing-library/react-hooks'; +import { + DefaultEntityFilters, + EntityListProvider, + useEntityList, +} from '../../hooks'; +import { catalogApiRef } from '../../api'; +import { + ApiRef, + IdentityApi, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { MemoryRouter } from 'react-router-dom'; +import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; +import { + EntityNamespaceFilter, + EntityOwnerFilter, + EntityUserListFilter, +} from '../../filters'; +import { useMountEffect } from '@react-hookz/web'; + +const mockQueryEntities: jest.MockedFn = jest.fn(); +const mockCatalogApi: jest.Mocked> = { + queryEntities: mockQueryEntities, +}; + +const mockGetBackstageIdentity: jest.MockedFn< + IdentityApi['getBackstageIdentity'] +> = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => { + const actual = jest.requireActual('@backstage/core-plugin-api'); + return { + ...actual, + useApi: (ref: ApiRef) => { + if (ref === catalogApiRef) { + return mockCatalogApi; + } + if (ref === identityApiRef) { + return { + getBackstageIdentity: mockGetBackstageIdentity, + }; + } + + return actual.useApi(ref); + }, + }; +}); + +describe('useOwnedEntitiesCount', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockGetBackstageIdentity.mockResolvedValue({ + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + userEntityRef: 'user:default/spiderman', + type: 'user', + }); + }); + + it(`shouldn't invoke queryEntities when filters are loading`, async () => { + mockQueryEntities.mockResolvedValue({ + items: [], + totalItems: 10, + pageInfo: {}, + }); + + const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + wrapper: createWrapperWithInitialFilters({}), + }); + + await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + + await expect( + waitFor(() => expect(mockQueryEntities).toHaveBeenCalled()), + ).rejects.toThrow(); + + expect(result.current).toEqual({ + count: 0, + loading: false, + filter: EntityUserListFilter.owned([ + 'user:default/spiderman', + 'user:group/a-group', + ]), + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + }); + }); + + it(`should properly apply the filters`, async () => { + mockQueryEntities.mockResolvedValue({ + items: [], + totalItems: 10, + pageInfo: {}, + }); + + const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + wrapper: createWrapperWithInitialFilters({ + namespace: new EntityNamespaceFilter(['a-namespace']), + }), + }); + + await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + + await waitFor(() => + expect(mockQueryEntities).toHaveBeenCalledWith({ + filter: { + 'metadata.namespace': ['a-namespace'], + 'relations.ownedBy': ['user:default/spiderman', 'user:group/a-group'], + }, + limit: 0, + }), + ); + + expect(result.current).toEqual({ + count: 10, + loading: false, + filter: EntityUserListFilter.owned([ + 'user:default/spiderman', + 'user:group/a-group', + ]), + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + }); + }); + + it(`should return count 0 without invoking queryEntities if owners filter doesn't have claims on common with logged in user`, async () => { + mockQueryEntities.mockResolvedValue({ + items: [], + totalItems: 10, + pageInfo: {}, + }); + + const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + wrapper: createWrapperWithInitialFilters({ + namespace: new EntityNamespaceFilter(['a-namespace']), + owners: new EntityOwnerFilter(['group:default/monsters']), + }), + }); + + await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + + await expect( + waitFor(() => expect(mockQueryEntities).toHaveBeenCalled()), + ).rejects.toThrow(); + + expect(result.current).toEqual({ + count: 0, + loading: false, + filter: EntityUserListFilter.owned([ + 'user:default/spiderman', + 'user:group/a-group', + ]), + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + }); + }); + + it(`should send claims in common between owners filter and logged in user`, async () => { + mockQueryEntities.mockResolvedValue({ + items: [], + totalItems: 10, + pageInfo: {}, + }); + + const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + wrapper: createWrapperWithInitialFilters({ + namespace: new EntityNamespaceFilter(['a-namespace']), + owners: new EntityOwnerFilter([ + 'group:default/monsters', + 'user:group/a-group', + ]), + }), + }); + + await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + + await waitFor(() => + expect(mockQueryEntities).toHaveBeenCalledWith({ + filter: { + 'metadata.namespace': ['a-namespace'], + 'relations.ownedBy': ['user:group/a-group'], + }, + limit: 0, + }), + ); + + expect(result.current).toEqual({ + count: 10, + loading: false, + filter: EntityUserListFilter.owned([ + 'user:default/spiderman', + 'user:group/a-group', + ]), + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + }); + }); +}); + +function createWrapperWithInitialFilters( + filters: Partial, +) { + function WrapFilters(props: PropsWithChildren<{}>) { + const { updateFilters } = useEntityList(); + + useMountEffect(() => { + updateFilters(filters); + }); + return <>{props.children}; + } + + return function Wrapper(props: PropsWithChildren<{}>) { + return ( + + + {props.children} + + + ); + }; +} From bd4c4cec5d52fc3104159f276c062896fd07ad3f Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 21 Jun 2023 10:42:04 +0200 Subject: [PATCH 33/70] catalog-react: fix UserListPicker tests Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/UserListPicker.test.tsx | 84 +++++++++++++++---- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index ed6c43d12e..f0b82b550e 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -19,7 +19,12 @@ import { fireEvent, render, waitFor, screen } from '@testing-library/react'; import { UserEntity } from '@backstage/catalog-model'; import { UserListPicker, UserListPickerProps } from './UserListPicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityTagFilter, EntityUserListFilter } from '../../filters'; +import { + EntityKindFilter, + EntityNamespaceFilter, + EntityTagFilter, + EntityUserListFilter, +} from '../../filters'; import { CatalogApi, QueryEntitiesInitialRequest, @@ -159,12 +164,19 @@ describe('', () => { it('renders filters', async () => { render( - + , ); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await waitFor(() => expect( screen.getAllByRole('menuitem').map(({ textContent }) => textContent), @@ -172,17 +184,25 @@ describe('', () => { ); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: {}, + filter: { + 'metadata.namespace': ['default'], + }, limit: 0, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: { 'metadata.name': ['e-1', 'e-2'] }, + filter: { + 'metadata.namespace': ['default'], + 'relations.ownedBy': ['user:default/testuser'], + }, + limit: 0, + }); + expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ + filter: { + 'metadata.namespace': ['default'], + 'metadata.name': ['e-1', 'e-2'], + }, limit: 1000, }); - expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: { 'relations.ownedBy': ['user:default/testuser'] }, - limit: 0, - }); }); it('respects other frontend filters in counts', async () => { @@ -223,16 +243,23 @@ describe('', () => { it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); - const queryParameters = { user: 'owned' }; + const queryParameters = { user: 'owned', kind: 'component' }; render( , ); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -241,15 +268,16 @@ describe('', () => { ); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: {}, + filter: { kind: 'component' }, limit: 0, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ - filter: { 'metadata.name': ['e-1', 'e-2'] }, + filter: { kind: 'component', 'metadata.name': ['e-1', 'e-2'] }, limit: 1000, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { + kind: 'component', 'relations.ownedBy': ['user:default/testuser'], }, limit: 0, @@ -285,7 +313,11 @@ describe('', () => { @@ -293,6 +325,10 @@ describe('', () => { , ); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); + await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ user: EntityUserListFilter.all(), @@ -304,7 +340,11 @@ describe('', () => { @@ -319,9 +359,14 @@ describe('', () => { describe('filter resetting', () => { let updateFilters: jest.Mock; - const Picker = (props: UserListPickerProps) => ( + const Picker = ({ ...props }: UserListPickerProps) => ( - + @@ -331,7 +376,7 @@ describe('', () => { updateFilters = jest.fn(); }); - describe(`when there are no owned entities match the filter`, () => { + describe(`when there are no owned entities matching the filter`, () => { it('does not reset the filter while entities are loading', async () => { mockCatalogApi.queryEntities?.mockImplementation( () => new Promise(() => {}), @@ -342,7 +387,10 @@ describe('', () => { await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), ); - expect(updateFilters).not.toHaveBeenCalled(); + + await expect( + waitFor(() => expect(updateFilters).toHaveBeenCalled()), + ).rejects.toThrow(); }); it('does not reset the filter while owned entities are loading', async () => { From 083d556ef6774c8ab3bf52fa886b34ea153f0b7e Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 22 Jun 2023 23:55:33 +0200 Subject: [PATCH 34/70] catalog: mock queryEntities Signed-off-by: Vincenzo Scamporlino --- .../CatalogPage/DefaultCatalogPage.test.tsx | 70 +++++++++++++++++-- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index b7d3ca3ed8..f16eaf4e93 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -14,7 +14,10 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; +import { + CatalogApi, + QueryEntitiesInitialRequest, +} from '@backstage/catalog-client'; import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { TableColumn, TableProps } from '@backstage/core-components'; import { @@ -36,7 +39,7 @@ import { renderInTestApp, } from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; -import { fireEvent, screen } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { createComponentRouteRef } from '../../routes'; import { CatalogTableRow } from '../CatalogTable'; @@ -49,10 +52,12 @@ describe('DefaultCatalogPage', () => { }); afterEach(() => { window.history.replaceState = origReplaceState; + + jest.clearAllMocks(); }); - const catalogApi: Partial = { - getEntities: () => + const catalogApi: jest.Mocked> = { + getEntities: jest.fn().mockImplementation(() => Promise.resolve({ items: [ { @@ -97,17 +102,59 @@ describe('DefaultCatalogPage', () => { }, ], }), - getLocationByRef: () => - Promise.resolve({ id: 'id', type: 'url', target: 'url' }), - getEntityFacets: async () => ({ + ), + getLocationByRef: jest + .fn() + .mockImplementation(() => + Promise.resolve({ id: 'id', type: 'url', target: 'url' }), + ), + getEntityFacets: jest.fn().mockImplementation(async () => ({ facets: { 'relations.ownedBy': [ { count: 1, value: 'group:default/not-tools' }, { count: 1, value: 'group:default/tools' }, ], }, + })), + queryEntities: jest.fn().mockImplementation(async request => { + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['relations.ownedBy'] + ) { + // owned entities + return { items: [], totalItems: 3, pageInfo: {} }; + } + + if ( + ( + (request as QueryEntitiesInitialRequest).filter as Record< + string, + string + > + )['metadata.name'] + ) { + // starred entities + return { + items: [ + { + apiVersion: '1', + kind: 'component', + metadata: { name: 'Entity1', namespace: 'default' }, + }, + ], + totalItems: 1, + pageInfo: {}, + }; + } + // all items + return { items: [], totalItems: 2, pageInfo: {} }; }), }; + const testProfile: Partial = { displayName: 'Display Name', }; @@ -183,6 +230,8 @@ describe('DefaultCatalogPage', () => { it('should render the default actions of an item in the grid', async () => { await renderWrapped(); + await waitFor(() => expect(catalogApi.queryEntities).toHaveBeenCalled()); + fireEvent.click(screen.getByTestId('user-picker-owned')); await expect( screen.findByText(/Owned components \(1\)/), @@ -215,6 +264,8 @@ describe('DefaultCatalogPage', () => { ]; await renderWrapped(); + await waitFor(() => expect(catalogApi.queryEntities).toHaveBeenCalled()); + fireEvent.click(screen.getByTestId('user-picker-owned')); await expect( screen.findByText(/Owned components \(1\)/), @@ -231,7 +282,10 @@ describe('DefaultCatalogPage', () => { // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { await renderWrapped(); + await waitFor(() => expect(catalogApi.queryEntities).toHaveBeenCalled()); + fireEvent.click(screen.getByTestId('user-picker-owned')); + await expect( screen.findByText(/Owned components \(1\)/), ).resolves.toBeInTheDocument(); @@ -252,6 +306,8 @@ describe('DefaultCatalogPage', () => { // entities defaulting to "owned" filter and not based on the selected filter it('should render the correct entities filtered on the selected filter', async () => { await renderWrapped(); + await waitFor(() => expect(catalogApi.queryEntities).toHaveBeenCalled()); + fireEvent.click(screen.getByTestId('user-picker-owned')); await expect( screen.findByText(/Owned components \(1\)/), From f858e6ee26b16e81a740e6ee77c59ddf0a5b6f14 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 23 Jun 2023 00:21:47 +0200 Subject: [PATCH 35/70] catalog: fix flaky namespace column test Signed-off-by: Vincenzo Scamporlino --- .../src/components/CatalogPage/DefaultCatalogPage.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index f16eaf4e93..b331c0e477 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -83,6 +83,7 @@ describe('DefaultCatalogPage', () => { kind: 'Component', metadata: { name: 'Entity2', + namespace: 'default', }, spec: { owner: 'not-tools', From 5447358dc9e7c817c0d8f470b185e8ba3a5bf93d Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 3 Oct 2023 17:13:04 +0200 Subject: [PATCH 36/70] catalog-react: use correct waitFor Signed-off-by: Vincenzo Scamporlino --- .../components/UserListPicker/useAllEntitiesCount.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx index 61c2c6eb0a..fc2c0e65bd 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx @@ -16,7 +16,6 @@ import React, { PropsWithChildren } from 'react'; import { CatalogApi } from '@backstage/catalog-client'; import { useAllEntitiesCount } from './useAllEntitiesCount'; -import { waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { EntityListProvider, useEntityList } from '../../hooks'; import { catalogApiRef } from '../../api'; @@ -62,7 +61,7 @@ describe('useAllEntitiesCount', () => { return <>{props.children}; } - const { result } = renderHook(() => useAllEntitiesCount(), { + const { result, waitFor } = renderHook(() => useAllEntitiesCount(), { wrapper: ({ children }) => ( @@ -90,7 +89,7 @@ describe('useAllEntitiesCount', () => { pageInfo: {}, }); - const { result } = renderHook(() => useAllEntitiesCount(), { + const { result, waitFor } = renderHook(() => useAllEntitiesCount(), { wrapper: ({ children }) => ( {children} From 62206785147963524c5995fbc2533631eb1c1647 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 3 Oct 2023 17:13:36 +0200 Subject: [PATCH 37/70] catalog-react: add useStarredEntitiesCount tests Signed-off-by: Vincenzo Scamporlino --- .../useStarredEntitiesCount.test.tsx | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx new file mode 100644 index 0000000000..1a02f3996b --- /dev/null +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx @@ -0,0 +1,124 @@ +/* + * 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 from 'react'; +import { CatalogApi } from '@backstage/catalog-client'; +import { EntityListProvider, useStarredEntities } from '../../hooks'; +import { catalogApiRef } from '../../api'; +import { ApiRef } from '@backstage/core-plugin-api'; +import { MemoryRouter } from 'react-router-dom'; +import { useStarredEntitiesCount } from './useStarredEntitiesCount'; +import { renderHook } from '@testing-library/react-hooks'; + +const mockQueryEntities: jest.MockedFn = jest.fn(); +const mockCatalogApi: jest.Mocked> = { + queryEntities: mockQueryEntities, +}; + +const mockStarredEntities: jest.MockedFn<() => Set> = jest.fn(); + +const mockUseStarredEntities: ReturnType = { + get starredEntities() { + return mockStarredEntities(); + }, +} as ReturnType; + +jest.mock('../../hooks', () => { + const actual = jest.requireActual('../../hooks'); + return { ...actual, useStarredEntities: () => mockUseStarredEntities }; +}); + +jest.mock('@backstage/core-plugin-api', () => { + const actual = jest.requireActual('@backstage/core-plugin-api'); + return { + ...actual, + useApi: (ref: ApiRef) => + ref === catalogApiRef ? mockCatalogApi : actual.useApi(ref), + }; +}); + +describe('useStarredEntitiesCount', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the count', async () => { + mockStarredEntities.mockReturnValue( + new Set(['component:default/favourite1', 'component:default/favourite2']), + ); + mockQueryEntities.mockResolvedValue({ + items: [ + { + apiVersion: '1', + kind: 'component', + metadata: { name: 'favourite1' }, + }, + { + apiVersion: '1', + kind: 'component', + metadata: { name: 'favourite2' }, + }, + ], + totalItems: 2, + pageInfo: {}, + }); + + const { result, waitFor } = renderHook(() => useStarredEntitiesCount(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => + expect(mockQueryEntities).toHaveBeenCalledWith({ + filter: { + 'metadata.name': ['favourite1', 'favourite2'], + }, + limit: 1000, + }), + ); + expect(result.current).toEqual({ + count: 2, + loading: false, + filter: { + refs: ['component:default/favourite1', 'component:default/favourite2'], + value: 'starred', + }, + }); + }); + + it(`shouldn't invoke the endpoint if there are no starred entities`, async () => { + mockStarredEntities.mockReturnValue(new Set()); + + const { result, waitFor } = renderHook(() => useStarredEntitiesCount(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await expect( + waitFor(() => expect(mockQueryEntities).toHaveBeenCalled()), + ).rejects.toThrow(); + expect(result.current).toEqual({ + count: 0, + loading: false, + filter: { refs: [], value: 'starred' }, + }); + }); +}); From 1fd53fa0c6e667e0601c417616cf1982582463aa Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 3 Oct 2023 17:30:01 +0200 Subject: [PATCH 38/70] add UserListPicker changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/shaggy-buses-beg.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/shaggy-buses-beg.md diff --git a/.changeset/shaggy-buses-beg.md b/.changeset/shaggy-buses-beg.md new file mode 100644 index 0000000000..d2c9351b5e --- /dev/null +++ b/.changeset/shaggy-buses-beg.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +The `UserListPicker` component has undergone improvements to enhance its performance. + +The previous implementation inferred the number of owned and starred entities based on the entities available in the `EntityListContext`. The updated version no longer relies on the `EntityListContext` for inference, allowing for better decoupling. + +The component now loads the entities' count asynchronously, resulting in improved performance and responsiveness. For this purpose, some of the exported filters such as `EntityTagFilter`, `EntityOwnerFilter`, `EntityLifecycleFilter` and `EntityNamespaceFilter` have now the `getCatalogFilters` method implemented. From db647a72fe9b7f05c9ec6c3f400cedb17b7cb367 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 3 Oct 2023 19:26:14 +0200 Subject: [PATCH 39/70] api-docs: fix flaky tests Signed-off-by: Vincenzo Scamporlino --- .../DefaultApiExplorerPage.test.tsx | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index fa698ded58..6d0b191d68 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -52,6 +52,10 @@ describe('DefaultApiExplorerPage', () => { kind: 'API', metadata: { name: 'Entity1', + annotations: { + 'backstage.io/view-url': 'viewurl', + 'backstage.io/edit-url': 'editurl', + }, }, spec: { type: 'openapi' }, }, @@ -63,6 +67,11 @@ describe('DefaultApiExplorerPage', () => { getEntityFacets: async () => ({ facets: { 'relations.ownedBy': [] }, }), + queryEntities: async () => ({ + items: [], + pageInfo: {}, + totalItems: 0, + }), }; const configApi: ConfigApi = new ConfigReader({ @@ -152,37 +161,39 @@ describe('DefaultApiExplorerPage', () => { it('should render the default actions of an item in the grid', async () => { await renderWrapped(); - expect(await screen.findByText(/All apis \(1\)/)).toBeInTheDocument(); - expect(await screen.findByTitle(/View/)).toBeInTheDocument(); - expect(await screen.findByTitle(/View/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Edit/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Add to favorites/)).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(/All apis \(1\)/)).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /view/i })).toBeInTheDocument(); + }); + expect(screen.getByRole('button', { name: /edit/i })).toBeInTheDocument(); + expect(screen.getByTitle(/Add to favorites/)).toBeInTheDocument(); }); it('should render the custom actions of an item passed as prop', async () => { const actions: TableProps['actions'] = [ - () => { - return { - icon: () => , - tooltip: 'Foo Action', - disabled: false, - onClick: () => jest.fn(), - }; + { + icon: () => , + tooltip: 'Foo Action', + disabled: false, + onClick: jest.fn(), }, - () => { - return { - icon: () => , - tooltip: 'Bar Action', - disabled: true, - onClick: () => jest.fn(), - }; + { + icon: () => , + tooltip: 'Bar Action', + disabled: true, + onClick: jest.fn(), }, ]; await renderWrapped(); - expect(await screen.findByText(/All apis \(1\)/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Foo Action/)).toBeInTheDocument(); - expect(await screen.findByTitle(/Bar Action/)).toBeInTheDocument(); - expect((await screen.findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + await waitFor(() => { + expect(screen.getByText(/All apis \(1\)/)).toBeInTheDocument(); + }); + expect(screen.getByTitle(/Foo Action/)).toBeInTheDocument(); + expect(screen.getByTitle(/Bar Action/)).toBeInTheDocument(); + expect(screen.getByTitle(/Bar Action/).firstChild).toBeDisabled(); }); }); From 4efc4c1cab820bf8176a425c1df8ec121975139e Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 9 Oct 2023 14:12:27 +0200 Subject: [PATCH 40/70] catalog-react: fix comments Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/UserListPicker.test.tsx | 13 ++++++------- plugins/catalog-react/src/filters.ts | 9 +++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index f0b82b550e..63228717a5 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -523,9 +523,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render( - , - ); /* picker({ loading: true })*/ + render(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -566,9 +564,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render( - , - ); /* picker({ loading: true })*/ + render(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -587,7 +583,10 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.starred(expect.any(Array)), + user: EntityUserListFilter.starred([ + 'component:default/e-1', + 'component:default/e-2', + ]), }), ); }); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index b94b538802..74ab771999 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -241,11 +241,11 @@ export class EntityUserListFilter implements EntityFilter { // This is supposed to return always true for paginated // owned entities, since the filters are applied server side. if (this.value === 'owned') { + const relations = getEntityRelations(entity, RELATION_OWNED_BY); + return ( this.refs?.some(v => - getEntityRelations(entity, RELATION_OWNED_BY).some( - o => stringifyEntityRef(o) === v, - ), + relations.some(o => stringifyEntityRef(o) === v), ) ?? false ); } @@ -309,9 +309,6 @@ export class EntityOrphanFilter implements EntityFilter { export class EntityErrorFilter implements EntityFilter { constructor(readonly value: boolean) {} - // TODO(vinzscam): is it possible to implement - // getCatalogFilters? ask mammals - filterEntity(entity: Entity): boolean { const error = ((entity as AlphaEntity)?.status?.items?.length as number) > 0; From 0aa3cecddfb201865d58811f0c3bb45026474509 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 9 Oct 2023 14:13:44 +0200 Subject: [PATCH 41/70] catalog: simplify typings Signed-off-by: Vincenzo Scamporlino --- .../CatalogPage/DefaultCatalogPage.test.tsx | 60 ++++++++----------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index b331c0e477..53f031e263 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -117,43 +117,31 @@ describe('DefaultCatalogPage', () => { ], }, })), - queryEntities: jest.fn().mockImplementation(async request => { - if ( - ( - (request as QueryEntitiesInitialRequest).filter as Record< - string, - string - > - )['relations.ownedBy'] - ) { - // owned entities - return { items: [], totalItems: 3, pageInfo: {} }; - } + queryEntities: jest + .fn() + .mockImplementation(async (request: QueryEntitiesInitialRequest) => { + if ((request.filter as any)['relations.ownedBy']) { + // owned entities + return { items: [], totalItems: 3, pageInfo: {} }; + } - if ( - ( - (request as QueryEntitiesInitialRequest).filter as Record< - string, - string - > - )['metadata.name'] - ) { - // starred entities - return { - items: [ - { - apiVersion: '1', - kind: 'component', - metadata: { name: 'Entity1', namespace: 'default' }, - }, - ], - totalItems: 1, - pageInfo: {}, - }; - } - // all items - return { items: [], totalItems: 2, pageInfo: {} }; - }), + if ((request.filter as any)['metadata.name']) { + // starred entities + return { + items: [ + { + apiVersion: '1', + kind: 'component', + metadata: { name: 'Entity1', namespace: 'default' }, + }, + ], + totalItems: 1, + pageInfo: {}, + }; + } + // all items + return { items: [], totalItems: 2, pageInfo: {} }; + }), }; const testProfile: Partial = { From 2dd206a2fd596e80e2673918e4965fd1599e8461 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 12 Oct 2023 13:25:47 +0200 Subject: [PATCH 42/70] catalog-react: fix orphan query Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/filters.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 74ab771999..3d871fe412 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -293,7 +293,10 @@ export class EntityOrphanFilter implements EntityFilter { constructor(readonly value: boolean) {} getCatalogFilters(): Record { - return { 'metadata.annotations.backstage.io/orphan': String(this.value) }; + if (this.value) { + return { 'metadata.annotations.backstage.io/orphan': String(this.value) }; + } + return {}; } filterEntity(entity: Entity): boolean { From 3ea09ae6ebfcc8b5d84ed7da8cc9068165b9053a Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 20 Oct 2023 08:08:03 +0200 Subject: [PATCH 43/70] catalog: flip the allowed backend filters logic Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/src/utils/filters.ts | 31 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index 5ab6e43593..f2cfe64960 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -16,7 +16,16 @@ import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '../types'; -import { EntityKindFilter, EntityTypeFilter } from '../filters'; +import { + EntityLifecycleFilter, + EntityNamespaceFilter, + EntityOrphanFilter, + EntityOwnerFilter, + EntityTagFilter, + EntityTextFilter, + EntityUserListFilter, + UserListFilter, +} from '../filters'; export function reduceCatalogFilters( filters: EntityFilter[], @@ -29,6 +38,13 @@ export function reduceCatalogFilters( }, {} as Record); } +/** + * This function computes and returns an object containing the filters to be sent + * to the backend. Any filter coming from `EntityKindFilter` and `EntityTypeFilter`, together + * with custom filter set by the adopters is allowed. This function is used by `EntityListProvider` + * and it won't be needed anymore in the future once pagination is implemented, as all the filters + * will be applied backend-side. + */ export function reduceBackendCatalogFilters(filters: EntityFilter[]) { const backendCatalogFilters: Record< string, @@ -37,11 +53,18 @@ export function reduceBackendCatalogFilters(filters: EntityFilter[]) { filters.forEach(filter => { if ( - filter instanceof EntityKindFilter || - filter instanceof EntityTypeFilter + filter instanceof EntityTagFilter || + filter instanceof EntityOwnerFilter || + filter instanceof EntityLifecycleFilter || + filter instanceof EntityNamespaceFilter || + filter instanceof EntityUserListFilter || + filter instanceof EntityOrphanFilter || + filter instanceof EntityTextFilter || + filter instanceof UserListFilter ) { - Object.assign(backendCatalogFilters, filter.getCatalogFilters()); + return; } + Object.assign(backendCatalogFilters, filter.getCatalogFilters?.() || {}); }); return backendCatalogFilters; From 89e0babcb8c6fd7b930a70bc62fc7d69090f70e1 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 20 Oct 2023 08:10:11 +0200 Subject: [PATCH 44/70] catalog-react: rename EntityUserListFilter to EntityUserFilter Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/UserListPicker.test.tsx | 22 +++++++++---------- .../UserListPicker/UserListPicker.tsx | 4 ++-- .../useOwnedEntitiesCount.test.tsx | 10 ++++----- .../UserListPicker/useOwnedEntitiesCount.ts | 4 ++-- .../UserListPicker/useStarredEntitiesCount.ts | 4 ++-- plugins/catalog-react/src/filters.ts | 10 ++++----- .../src/hooks/useEntityListProvider.test.tsx | 6 ++--- .../src/hooks/useEntityListProvider.tsx | 4 ++-- plugins/catalog-react/src/utils/filters.ts | 4 ++-- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 63228717a5..d7572c2ac3 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -23,7 +23,7 @@ import { EntityKindFilter, EntityNamespaceFilter, EntityTagFilter, - EntityUserListFilter, + EntityUserFilter, } from '../../filters'; import { CatalogApi, @@ -263,7 +263,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.owned(ownershipEntityRefs), + user: EntityUserFilter.owned(ownershipEntityRefs), }), ); @@ -298,7 +298,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.starred([ + user: EntityUserFilter.starred([ 'component:default/e-1', 'component:default/e-2', ]), @@ -331,7 +331,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.all(), + user: EntityUserFilter.all(), }), ); @@ -352,7 +352,7 @@ describe('', () => { , ); expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.owned(ownershipEntityRefs), + user: EntityUserFilter.owned(ownershipEntityRefs), }); }); @@ -437,7 +437,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.all(), + user: EntityUserFilter.all(), }), ); }); @@ -501,7 +501,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.all(), + user: EntityUserFilter.all(), }), ); }); @@ -529,7 +529,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), ); expect(updateFilters).not.toHaveBeenCalledWith({ - user: EntityUserListFilter.all(), + user: EntityUserFilter.all(), }); }); @@ -542,7 +542,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.owned(expect.any(Array)), + user: EntityUserFilter.owned(expect.any(Array)), }), ); }); @@ -570,7 +570,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), ); expect(updateFilters).not.toHaveBeenCalledWith({ - user: EntityUserListFilter.all(), + user: EntityUserFilter.all(), }); }); @@ -583,7 +583,7 @@ describe('', () => { await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ - user: EntityUserListFilter.starred([ + user: EntityUserFilter.starred([ 'component:default/e-1', 'component:default/e-2', ]), diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index b0a86b1322..854a1cb1b7 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -33,7 +33,7 @@ import { import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; -import { EntityUserListFilter } from '../../filters'; +import { EntityUserFilter } from '../../filters'; import { useEntityList } from '../../hooks'; import { UserListFilterKind } from '../../types'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; @@ -213,7 +213,7 @@ export const UserListPicker = (props: UserListPickerProps) => { if (selectedUserFilter === 'starred') { return starredEntitiesFilter; } - return EntityUserListFilter.all(); + return EntityUserFilter.all(); }; updateFilters({ user: getFilter() }); diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 6a79e63c8f..4ce3503394 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -32,7 +32,7 @@ import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; import { EntityNamespaceFilter, EntityOwnerFilter, - EntityUserListFilter, + EntityUserFilter, } from '../../filters'; import { useMountEffect } from '@react-hookz/web'; @@ -95,7 +95,7 @@ describe('useOwnedEntitiesCount', () => { expect(result.current).toEqual({ count: 0, loading: false, - filter: EntityUserListFilter.owned([ + filter: EntityUserFilter.owned([ 'user:default/spiderman', 'user:group/a-group', ]), @@ -131,7 +131,7 @@ describe('useOwnedEntitiesCount', () => { expect(result.current).toEqual({ count: 10, loading: false, - filter: EntityUserListFilter.owned([ + filter: EntityUserFilter.owned([ 'user:default/spiderman', 'user:group/a-group', ]), @@ -162,7 +162,7 @@ describe('useOwnedEntitiesCount', () => { expect(result.current).toEqual({ count: 0, loading: false, - filter: EntityUserListFilter.owned([ + filter: EntityUserFilter.owned([ 'user:default/spiderman', 'user:group/a-group', ]), @@ -202,7 +202,7 @@ describe('useOwnedEntitiesCount', () => { expect(result.current).toEqual({ count: 10, loading: false, - filter: EntityUserListFilter.owned([ + filter: EntityUserFilter.owned([ 'user:default/spiderman', 'user:group/a-group', ]), diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts index 605c006f44..d86610d9ae 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -20,7 +20,7 @@ import { compact, intersection, isEqual } from 'lodash'; import { useMemo, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; -import { EntityOwnerFilter, EntityUserListFilter } from '../../filters'; +import { EntityOwnerFilter, EntityUserFilter } from '../../filters'; import { useEntityList } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; @@ -82,7 +82,7 @@ export function useOwnedEntitiesCount() { const loading = loadingEntityRefs || loadingEntityOwnership; const filter = useMemo( - () => EntityUserListFilter.owned(ownershipEntityRefs ?? []), + () => EntityUserFilter.owned(ownershipEntityRefs ?? []), [ownershipEntityRefs], ); diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts index 93adad0489..6fa49d37a5 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts @@ -21,7 +21,7 @@ import { compact, isEqual } from 'lodash'; import { useMemo, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; -import { EntityUserListFilter } from '../../filters'; +import { EntityUserFilter } from '../../filters'; import { useEntityList, useStarredEntities } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; @@ -72,7 +72,7 @@ export function useStarredEntitiesCount() { }, [request, starredEntities]); const filter = useMemo( - () => EntityUserListFilter.starred(Array.from(starredEntities)), + () => EntityUserFilter.starred(Array.from(starredEntities)), [starredEntities], ); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 3d871fe412..55983d124b 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -203,22 +203,22 @@ export class EntityNamespaceFilter implements EntityFilter { /** * @public */ -export class EntityUserListFilter implements EntityFilter { +export class EntityUserFilter implements EntityFilter { private constructor( readonly value: UserListFilterKind, readonly refs?: string[], ) {} static owned(ownershipEntityRefs: string[]) { - return new EntityUserListFilter('owned', ownershipEntityRefs); + return new EntityUserFilter('owned', ownershipEntityRefs); } static all() { - return new EntityUserListFilter('all'); + return new EntityUserFilter('all'); } static starred(starredEntityRefs: string[]) { - return new EntityUserListFilter('starred', starredEntityRefs); + return new EntityUserFilter('starred', starredEntityRefs); } getCatalogFilters(): Record { @@ -259,7 +259,7 @@ export class EntityUserListFilter implements EntityFilter { /** * Filters entities based on whatever the user has starred or owns them. - * @deprecated use EntityUserListFilter + * @deprecated use EntityUserFilter * @public */ export class UserListFilter implements EntityFilter { diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index a4401e3441..edbfb65c0c 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -35,7 +35,7 @@ import { EntityKindPicker, UserListPicker } from '../components'; import { EntityKindFilter, EntityTypeFilter, - EntityUserListFilter, + EntityUserFilter, } from '../filters'; import { UserListFilterKind } from '../types'; import { EntityListProvider, useEntityList } from './useEntityListProvider'; @@ -155,7 +155,7 @@ describe('', () => { act(() => result.current.updateFilters({ - user: EntityUserListFilter.owned(ownershipEntityRefs), + user: EntityUserFilter.owned(ownershipEntityRefs), }), ); @@ -196,7 +196,7 @@ describe('', () => { act(() => result.current.updateFilters({ - user: EntityUserListFilter.owned(ownershipEntityRefs), + user: EntityUserFilter.owned(ownershipEntityRefs), }), ); diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index e2a8cfc657..948efc969f 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -41,7 +41,7 @@ import { EntityTypeFilter, UserListFilter, EntityNamespaceFilter, - EntityUserListFilter, + EntityUserFilter, } from '../filters'; import { EntityFilter } from '../types'; import { reduceBackendCatalogFilters, reduceEntityFilters } from '../utils'; @@ -51,7 +51,7 @@ import { useApi } from '@backstage/core-plugin-api'; export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; - user?: UserListFilter | EntityUserListFilter; + user?: UserListFilter | EntityUserFilter; owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index f2cfe64960..580d7b0b76 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -23,7 +23,7 @@ import { EntityOwnerFilter, EntityTagFilter, EntityTextFilter, - EntityUserListFilter, + EntityUserFilter, UserListFilter, } from '../filters'; @@ -57,7 +57,7 @@ export function reduceBackendCatalogFilters(filters: EntityFilter[]) { filter instanceof EntityOwnerFilter || filter instanceof EntityLifecycleFilter || filter instanceof EntityNamespaceFilter || - filter instanceof EntityUserListFilter || + filter instanceof EntityUserFilter || filter instanceof EntityOrphanFilter || filter instanceof EntityTextFilter || filter instanceof UserListFilter From 67ee8f155f74764e119856d2801702b48a59c9be Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 20 Oct 2023 08:19:36 +0200 Subject: [PATCH 45/70] catalog-react: add clarify comments Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/useStarredEntitiesCount.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts index 6fa49d37a5..f7b2b101f3 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.ts @@ -41,8 +41,17 @@ export function useStarredEntitiesCount() { const newRequest: QueryEntitiesInitialRequest = { filter: { ...filter, + /** + * here we are filtering entities by `name`. Given this filter, + * the response might contain more entities than expected, in case multiple entities + * of different kind or namespace share the same name. Those extra entities are filtered out + * client side by `EntityUserFilter`, so they won't be visible to the user. + */ [facet]: Array.from(starredEntities).map(e => parseEntityRef(e).name), }, + /** + * limit is set to a high value as we are not expecting many starred entities + */ limit: 1000, }; if (isEqual(newRequest, prevRequest.current)) { @@ -58,6 +67,12 @@ export function useStarredEntitiesCount() { return 0; } + /** + * given a list of starred entity refs and some filters coming from CatalogPage, + * it reduces the list of starred entities, to a list of entities that matches the + * provided filters. It won't be possible to getEntitiesByRefs + * as the method doesn't accept any filter. + */ const response = await catalogApi.queryEntities(request); return response.items From 4078bd73bbf7a9df06c8f3b5a6a169686dff9546 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 20 Oct 2023 13:16:46 +0200 Subject: [PATCH 46/70] catalog-react: fixes for react 18 Signed-off-by: Vincenzo Scamporlino --- .../UserListPicker/UserListPicker.test.tsx | 13 +++---- .../useAllEntitiesCount.test.tsx | 6 ++-- .../useOwnedEntitiesCount.test.tsx | 12 +++---- .../UserListPicker/useOwnedEntitiesCount.ts | 36 ++++++++++++++----- .../useStarredEntitiesCount.test.tsx | 29 ++++++++------- .../src/hooks/useStarredEntities.test.tsx | 3 +- .../src/hooks/useStarredEntity.test.tsx | 3 +- 7 files changed, 61 insertions(+), 41 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index d7572c2ac3..ad56681d6c 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -142,6 +142,7 @@ describe('', () => { afterEach(() => { jest.resetAllMocks(); }); + it('renders filter groups', async () => { render( @@ -357,7 +358,7 @@ describe('', () => { }); describe('filter resetting', () => { - let updateFilters: jest.Mock; + const updateFilters = jest.fn(); const Picker = ({ ...props }: UserListPickerProps) => ( @@ -372,15 +373,9 @@ describe('', () => { ); - beforeEach(() => { - updateFilters = jest.fn(); - }); - describe(`when there are no owned entities matching the filter`, () => { it('does not reset the filter while entities are loading', async () => { - mockCatalogApi.queryEntities?.mockImplementation( - () => new Promise(() => {}), - ); + mockCatalogApi.queryEntities?.mockReturnValue(new Promise(() => {})); render(); @@ -388,7 +383,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), ); - await expect( + await expect(() => waitFor(() => expect(updateFilters).toHaveBeenCalled()), ).rejects.toThrow(); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx index fc2c0e65bd..dc3bc5d2bd 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx @@ -16,7 +16,7 @@ import React, { PropsWithChildren } from 'react'; import { CatalogApi } from '@backstage/catalog-client'; import { useAllEntitiesCount } from './useAllEntitiesCount'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; import { EntityListProvider, useEntityList } from '../../hooks'; import { catalogApiRef } from '../../api'; import { ApiRef } from '@backstage/core-plugin-api'; @@ -61,7 +61,7 @@ describe('useAllEntitiesCount', () => { return <>{props.children}; } - const { result, waitFor } = renderHook(() => useAllEntitiesCount(), { + const { result } = renderHook(() => useAllEntitiesCount(), { wrapper: ({ children }) => ( @@ -89,7 +89,7 @@ describe('useAllEntitiesCount', () => { pageInfo: {}, }); - const { result, waitFor } = renderHook(() => useAllEntitiesCount(), { + const { result } = renderHook(() => useAllEntitiesCount(), { wrapper: ({ children }) => ( {children} diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 4ce3503394..1f6fbfd53d 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -15,7 +15,7 @@ */ import React, { PropsWithChildren } from 'react'; import { CatalogApi } from '@backstage/catalog-client'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; import { DefaultEntityFilters, EntityListProvider, @@ -82,7 +82,7 @@ describe('useOwnedEntitiesCount', () => { pageInfo: {}, }); - const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + const { result } = renderHook(() => useOwnedEntitiesCount(), { wrapper: createWrapperWithInitialFilters({}), }); @@ -110,7 +110,7 @@ describe('useOwnedEntitiesCount', () => { pageInfo: {}, }); - const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + const { result } = renderHook(() => useOwnedEntitiesCount(), { wrapper: createWrapperWithInitialFilters({ namespace: new EntityNamespaceFilter(['a-namespace']), }), @@ -139,14 +139,14 @@ describe('useOwnedEntitiesCount', () => { }); }); - it(`should return count 0 without invoking queryEntities if owners filter doesn't have claims on common with logged in user`, async () => { + it(`should return count 0 without invoking queryEntities if owners filter doesn't have claims in common with logged in user`, async () => { mockQueryEntities.mockResolvedValue({ items: [], totalItems: 10, pageInfo: {}, }); - const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + const { result } = renderHook(() => useOwnedEntitiesCount(), { wrapper: createWrapperWithInitialFilters({ namespace: new EntityNamespaceFilter(['a-namespace']), owners: new EntityOwnerFilter(['group:default/monsters']), @@ -177,7 +177,7 @@ describe('useOwnedEntitiesCount', () => { pageInfo: {}, }); - const { result, waitFor } = renderHook(() => useOwnedEntitiesCount(), { + const { result } = renderHook(() => useOwnedEntitiesCount(), { wrapper: createWrapperWithInitialFilters({ namespace: new EntityNamespaceFilter(['a-namespace']), owners: new EntityOwnerFilter([ diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts index d86610d9ae..1adab49361 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.ts @@ -17,12 +17,13 @@ import { QueryEntitiesInitialRequest } from '@backstage/catalog-client'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { compact, intersection, isEqual } from 'lodash'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; import { EntityOwnerFilter, EntityUserFilter } from '../../filters'; import { useEntityList } from '../../hooks'; import { reduceCatalogFilters } from '../../utils'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; export function useOwnedEntitiesCount() { const identityApi = useApi(identityApiRef); @@ -71,14 +72,33 @@ export function useOwnedEntitiesCount() { return newRequest; }, [filters, ownershipEntityRefs]); - const { value: count, loading: loadingEntityOwnership } = - useAsync(async () => { - if (!request) { - return 0; + const [{ value: count, loading: loadingEntityOwnership }, fetchEntities] = + useAsyncFn( + async ( + req: QueryEntitiesInitialRequest | undefined, + ownershipEntityRefsParam: string[], + ) => { + if (ownershipEntityRefsParam && !req) { + // this implicitly means that there aren't claims in common with + // the logged in users, so avoid invoking the queryEntities endpoint + // which will implicitly returns 0 + return 0; + } + const { totalItems } = await catalogApi.queryEntities(req); + return totalItems; + }, + [], + { loading: true }, + ); + + useEffect(() => { + if (ownershipEntityRefs) { + if (request && Object.keys(request).length === 0) { + return; } - const { totalItems } = await catalogApi.queryEntities(request); - return totalItems; - }, [request]); + fetchEntities(request, ownershipEntityRefs); + } + }, [fetchEntities, request, ownershipEntityRefs]); const loading = loadingEntityRefs || loadingEntityOwnership; const filter = useMemo( diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx index 1a02f3996b..5fe648e1bc 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx @@ -20,7 +20,7 @@ import { catalogApiRef } from '../../api'; import { ApiRef } from '@backstage/core-plugin-api'; import { MemoryRouter } from 'react-router-dom'; import { useStarredEntitiesCount } from './useStarredEntitiesCount'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; const mockQueryEntities: jest.MockedFn = jest.fn(); const mockCatalogApi: jest.Mocked> = { @@ -75,7 +75,7 @@ describe('useStarredEntitiesCount', () => { pageInfo: {}, }); - const { result, waitFor } = renderHook(() => useStarredEntitiesCount(), { + const { result } = renderHook(() => useStarredEntitiesCount(), { wrapper: ({ children }) => ( {children} @@ -83,28 +83,31 @@ describe('useStarredEntitiesCount', () => { ), }); - await waitFor(() => + await waitFor(() => { expect(mockQueryEntities).toHaveBeenCalledWith({ filter: { 'metadata.name': ['favourite1', 'favourite2'], }, limit: 1000, - }), - ); - expect(result.current).toEqual({ - count: 2, - loading: false, - filter: { - refs: ['component:default/favourite1', 'component:default/favourite2'], - value: 'starred', - }, + }); + expect(result.current).toEqual({ + count: 2, + loading: false, + filter: { + refs: [ + 'component:default/favourite1', + 'component:default/favourite2', + ], + value: 'starred', + }, + }); }); }); it(`shouldn't invoke the endpoint if there are no starred entities`, async () => { mockStarredEntities.mockReturnValue(new Set()); - const { result, waitFor } = renderHook(() => useStarredEntitiesCount(), { + const { result } = renderHook(() => useStarredEntitiesCount(), { wrapper: ({ children }) => ( {children} diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index e3010a0f4a..07738fd3d9 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -16,7 +16,8 @@ import { Entity } from '@backstage/catalog-model'; import { TestApiProvider } from '@backstage/test-utils'; -import { act, renderHook, waitFor } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { starredEntitiesApiRef, diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx index e64a9bbdf8..44a4c7aaa3 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx @@ -16,7 +16,8 @@ import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { TestApiProvider } from '@backstage/test-utils'; -import { renderHook, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import Observable from 'zen-observable'; import { StarredEntitiesApi, starredEntitiesApiRef } from '../apis'; From 89748e3c2dc5768cf9f1d6938d6a7acfbd8ee2c9 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 20 Oct 2023 15:53:34 +0200 Subject: [PATCH 47/70] catalog: wait for async operation Signed-off-by: Vincenzo Scamporlino --- .../src/components/CatalogPage/DefaultCatalogPage.test.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index 53f031e263..0ced712344 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -320,10 +320,9 @@ describe('DefaultCatalogPage', () => { // Now that we've starred an entity, the "Starred" menu option should be // enabled. - expect(screen.getByTestId('user-picker-starred')).not.toHaveAttribute( - 'aria-disabled', - 'true', - ); + expect( + await screen.findByTestId('user-picker-starred'), + ).not.toHaveAttribute('aria-disabled', 'true'); fireEvent.click(screen.getByTestId('user-picker-starred')); await expect( screen.findByText(/Starred components \(1\)/), From ff2131abe3f39249a02f8e3f6bb17be2db2b3fe4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 20 Oct 2023 17:46:22 +0200 Subject: [PATCH 48/70] catalog-react: update API report Signed-off-by: Patrik Oldsberg --- plugins/catalog-react/api-report.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index e73441f7dc..7422440955 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -142,7 +142,7 @@ export const columnFactories: Readonly<{ export type DefaultEntityFilters = { kind?: EntityKindFilter; type?: EntityTypeFilter; - user?: UserListFilter | EntityUserListFilter; + user?: UserListFilter | EntityUserFilter; owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; @@ -508,19 +508,19 @@ export interface EntityTypePickerProps { } // @public (undocumented) -export class EntityUserListFilter implements EntityFilter { +export class EntityUserFilter implements EntityFilter { // (undocumented) - static all(): EntityUserListFilter; + static all(): EntityUserFilter; // (undocumented) filterEntity(entity: Entity): boolean; // (undocumented) getCatalogFilters(): Record; // (undocumented) - static owned(ownershipEntityRefs: string[]): EntityUserListFilter; + static owned(ownershipEntityRefs: string[]): EntityUserFilter; // (undocumented) readonly refs?: string[] | undefined; // (undocumented) - static starred(starredEntityRefs: string[]): EntityUserListFilter; + static starred(starredEntityRefs: string[]): EntityUserFilter; // (undocumented) toQueryValue(): string; // (undocumented) From 26ca97ebaa6adc6e334b0682e61fc6ac3ff6e70d Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sat, 21 Oct 2023 19:51:47 +0530 Subject: [PATCH 49/70] Add examples for gitlab:projectAccessToken:create scaffolder action Signed-off-by: parmar-abhinav --- .changeset/wicked-ties-knock.md | 5 + .../package.json | 1 + ...bProjectAccessTokenAction.examples.test.ts | 303 ++++++++++++++++++ ...GitlabProjectAccessTokenAction.examples.ts | 104 ++++++ .../createGitlabProjectAccessTokenAction.ts | 2 + yarn.lock | 1 + 6 files changed, 416 insertions(+) create mode 100644 .changeset/wicked-ties-knock.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.ts diff --git a/.changeset/wicked-ties-knock.md b/.changeset/wicked-ties-knock.md new file mode 100644 index 0000000000..582cb905d0 --- /dev/null +++ b/.changeset/wicked-ties-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Add examples for `gitlab:projectAccessToken:create` scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 99c84ed4b9..deecc3b333 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -36,6 +36,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@gitbeaker/node": "^35.8.0", + "yaml": "^2.0.0", "zod": "^3.21.4" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts new file mode 100644 index 0000000000..6a809fc386 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts @@ -0,0 +1,303 @@ +/* + * 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 yaml from 'yaml'; +import { createGitlabProjectAccessTokenAction } from './createGitlabProjectAccessTokenAction'; // Adjust the import based on your project structure +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { examples } from './createGitlabProjectAccessTokenAction.examples'; + +jest.mock('node-fetch'); + +const mockGitlabClient = { + ProjectDeployTokens: { + create: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:projectAccessToken:create examples', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGitlabProjectAccessTokenAction({ integrations }); + + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('Create a GitLab project access token with minimal options.', async () => { + const fetchMock = jest.spyOn(global, 'fetch'); + const mockResponse = { + token: 'mock-access-token', + }; + + fetchMock.mockResolvedValue({ + json: async () => mockResponse, + } as Response); + + jest.mock('../util', () => ({ + getToken: jest.fn().mockReturnValue({ + token: 'mock-api-token', + integrationConfig: { config: { baseUrl: 'https://api.gitlab.com' } }, + }), + })); + + const input = yaml.parse(examples[0].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://gitlab.com/api/v4/projects/456/access_tokens', + { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': 'tokenlols', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: undefined, + scopes: undefined, + access_level: undefined, + }), + }, + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'mock-access-token', + ); + }); + + it('Create a GitLab project access token with custom scopes.', async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + const mockResponse = { + token: 'mock-access-token', + }; + + const fetchMock = jest.spyOn(global, 'fetch'); + fetchMock.mockResolvedValue({ + json: async () => mockResponse, + } as Response); + + jest.mock('../util', () => ({ + getToken: jest.fn().mockReturnValue({ + token: 'mock-api-token', + integrationConfig: { config: { baseUrl: 'https://api.gitlab.com' } }, + }), + })); + + await action.handler({ + ...mockContext, + input, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://gitlab.com/api/v4/projects/789/access_tokens', + { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': 'tokenlols', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: undefined, + scopes: ['read_registry', 'write_repository'], + access_level: undefined, + }), + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'mock-access-token', + ); + }); + + it('Create a GitLab project access token with a specified name.', async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + const mockResponse = { + token: 'mock-access-token', + }; + + const fetchMock = jest.spyOn(global, 'fetch'); + fetchMock.mockResolvedValue({ + json: async () => mockResponse, + } as Response); + + jest.mock('../util', () => ({ + getToken: jest.fn().mockReturnValue({ + token: 'mock-api-token', + integrationConfig: { config: { baseUrl: 'https://api.gitlab.com' } }, + }), + })); + + await action.handler({ + ...mockContext, + input, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://gitlab.com/api/v4/projects/101112/access_tokens', + { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': 'tokenlols', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'my-custom-token', + scopes: undefined, + access_level: undefined, + }), + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'mock-access-token', + ); + }); + + it('Create a GitLab project access token with a numeric project ID.', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + const mockResponse = { + token: 'mock-access-token', + }; + + const fetchMock = jest.spyOn(global, 'fetch'); + fetchMock.mockResolvedValue({ + json: async () => mockResponse, + } as Response); + + jest.mock('../util', () => ({ + getToken: jest.fn().mockReturnValue({ + token: 'mock-api-token', + integrationConfig: { config: { baseUrl: 'https://api.gitlab.com' } }, + }), + })); + + await action.handler({ + ...mockContext, + input, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://gitlab.com/api/v4/projects/42/access_tokens', + { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': 'tokenlols', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: undefined, + scopes: undefined, + access_level: undefined, + }), + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'mock-access-token', + ); + }); + + it('Create a GitLab project access token using specific GitLab integrations.', async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + const mockResponse = { + token: 'mock-access-token', + }; + + const fetchMock = jest.spyOn(global, 'fetch'); + fetchMock.mockResolvedValue({ + json: async () => mockResponse, + } as Response); + + jest.mock('../util', () => ({ + getToken: jest.fn().mockReturnValue({ + token: 'mock-api-token', + integrationConfig: { config: { baseUrl: 'https://api.gitlab.com' } }, + }), + })); + + await action.handler({ + ...mockContext, + input, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://gitlab.com/api/v4/projects/123/access_tokens', + { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': 'tokenlols', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: undefined, + scopes: undefined, + access_level: undefined, + }), + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'mock-access-token', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.ts new file mode 100644 index 0000000000..5fe157badf --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.ts @@ -0,0 +1,104 @@ +/* + * 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a GitLab project access token with minimal options.', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with custom scopes.', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '789', + scopes: ['read_registry', 'write_repository'], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with a specified name.', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + name: 'my-custom-token', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a numeric project ID.', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 42, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token using specific GitLab integrations.', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '123', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts index 614de0e221..a050b3c1b4 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts @@ -19,6 +19,7 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import commonGitlabConfig from '../commonGitlabConfig'; import { getToken } from '../util'; import { z } from 'zod'; +import { examples } from './createGitlabProjectAccessTokenAction.examples'; /** * Creates a `gitlab:projectAccessToken:create` Scaffolder action. @@ -32,6 +33,7 @@ export const createGitlabProjectAccessTokenAction = (options: { const { integrations } = options; return createTemplateAction({ id: 'gitlab:projectAccessToken:create', + examples, schema: { input: commonGitlabConfig.merge( z.object({ diff --git a/yarn.lock b/yarn.lock index 059c18d859..2ad8f4458e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8487,6 +8487,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@gitbeaker/node": ^35.8.0 + yaml: ^2.0.0 zod: ^3.21.4 languageName: unknown linkType: soft From 0a7c9ede2e7203bde1168a994972c9e57e342af4 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 21 Oct 2023 10:41:32 -0500 Subject: [PATCH 50/70] Refactored to use positive return Signed-off-by: Andre Wanlin --- .../getAnnotationValuesFromEntity.test.ts | 12 ++--- .../utils/getAnnotationValuesFromEntity.ts | 54 ++++++------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index 0d7a11f48b..a5d3725716 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -61,7 +61,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "project"', ); }); }); @@ -85,7 +85,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "/repo"', ); }); }); @@ -109,7 +109,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "project/"', ); }); }); @@ -255,7 +255,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/host-org was not in the correct format: /', + 'Invalid value for annotation "dev.azure.com/host-org"; expected format is: /, found: "host"', ); }); }); @@ -279,7 +279,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Host for annotation dev.azure.com/host-org was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/host-org"; expected format is: /, found: "/org"', ); }); }); @@ -303,7 +303,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Organization for annotation dev.azure.com/host-org was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/host-org"; expected format is: /, found: "host/"', ); }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 7099352524..99b194a834 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -69,27 +69,16 @@ function getProjectRepo(annotations?: Record): { return { project: undefined, repo: undefined }; } - if (!annotation.includes('/')) { - throw new Error( - `Value for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not in the correct format: /`, - ); + if (annotation.includes('/')) { + const [project, repo] = annotation.split('/'); + if (project && repo) { + return { project, repo }; + } } - const [project, repo] = annotation.split('/'); - - if (!project) { - throw new Error( - `Project Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, - ); - } - - if (!repo) { - throw new Error( - `Repo Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, - ); - } - - return { project, repo }; + throw new Error( + `Invalid value for annotation "${AZURE_DEVOPS_REPO_ANNOTATION}"; expected format is: /, found: "${annotation}"`, + ); } function getHostOrg(annotations?: Record): { @@ -101,25 +90,14 @@ function getHostOrg(annotations?: Record): { return { host: undefined, org: undefined }; } - if (!annotation.includes('/')) { - throw new Error( - `Value for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not in the correct format: /`, - ); + if (annotation.includes('/')) { + const [host, org] = annotation.split('/'); + if (host && org) { + return { host, org }; + } } - const [host, org] = annotation.split('/'); - - if (!host) { - throw new Error( - `Host for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, - ); - } - - if (!org) { - throw new Error( - `Organization for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, - ); - } - - return { host, org }; + throw new Error( + `Invalid value for annotation "${AZURE_DEVOPS_HOST_ORG_ANNOTATION}"; expected format is: /, found: "${annotation}"`, + ); } From 5578328e57908f4e0b4a809234b311688ad41fe3 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 21 Oct 2023 12:14:10 -0500 Subject: [PATCH 51/70] Added tests for changed hooks Signed-off-by: Andre Wanlin --- .../src/hooks/useGitTags.test.tsx | 130 ++++++++++++++++++ plugins/azure-devops/src/hooks/useGitTags.ts | 5 +- .../src/hooks/usePullRequests.test.tsx | 129 +++++++++++++++++ .../azure-devops/src/hooks/usePullRequests.ts | 5 +- .../src/hooks/useRepoBuilds.test.tsx | 126 +++++++++++++++++ .../azure-devops/src/hooks/useRepoBuilds.ts | 5 +- .../getAnnotationValuesFromEntity.test.ts | 4 +- .../utils/getAnnotationValuesFromEntity.ts | 4 +- 8 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 plugins/azure-devops/src/hooks/useGitTags.test.tsx create mode 100644 plugins/azure-devops/src/hooks/usePullRequests.test.tsx create mode 100644 plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx diff --git a/plugins/azure-devops/src/hooks/useGitTags.test.tsx b/plugins/azure-devops/src/hooks/useGitTags.test.tsx new file mode 100644 index 0000000000..1a5620e16e --- /dev/null +++ b/plugins/azure-devops/src/hooks/useGitTags.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { useGitTags } from './useGitTags'; +import { Entity } from '@backstage/catalog-model'; +import { GitTag } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; + +describe('useGitTags', () => { + const azureDevOpsApiMock = { + getGitTags: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide an array of GitTag', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const tags: GitTag[] = [ + { + objectId: 'tag-1', + peeledObjectId: 'tag-1', + name: 'tag-1', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-1', + }, + { + objectId: 'tag-2', + peeledObjectId: 'tag-2', + name: 'tag-2', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-2', + }, + { + objectId: 'tag-3', + peeledObjectId: 'tag-3', + name: 'tag-3', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-3', + }, + ]; + azureDevOpsApiMock.getGitTags.mockResolvedValue({ items: tags }); + const { result } = renderHook(() => useGitTags(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current).toEqual({ + error: undefined, + items: tags, + loading: false, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => useGitTags(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => useGitTags(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/useGitTags.ts b/plugins/azure-devops/src/hooks/useGitTags.ts index f36f9224aa..13c7118934 100644 --- a/plugins/azure-devops/src/hooks/useGitTags.ts +++ b/plugins/azure-devops/src/hooks/useGitTags.ts @@ -31,10 +31,7 @@ export function useGitTags(entity: Entity): { const { project, repo } = getAnnotationValuesFromEntity(entity); const { value, loading, error } = useAsync(async () => { - if (repo) { - return await api.getGitTags(project, repo); - } - return undefined; + return await api.getGitTags(project, repo as string); }, [api, project, repo]); return { diff --git a/plugins/azure-devops/src/hooks/usePullRequests.test.tsx b/plugins/azure-devops/src/hooks/usePullRequests.test.tsx new file mode 100644 index 0000000000..3e51c003e6 --- /dev/null +++ b/plugins/azure-devops/src/hooks/usePullRequests.test.tsx @@ -0,0 +1,129 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; +import { PullRequest } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; +import { usePullRequests } from './usePullRequests'; + +describe('usePullRequests', () => { + const azureDevOpsApiMock = { + getPullRequests: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide an array of PullRequest', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const pullRequests: PullRequest[] = [ + { + pullRequestId: 1, + repoName: 'repo', + title: 'title-1', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + pullRequestId: 2, + repoName: 'repo', + title: 'title-2', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + pullRequestId: 3, + repoName: 'repo', + title: 'title-3', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + ]; + azureDevOpsApiMock.getPullRequests.mockResolvedValue({ + items: pullRequests, + }); + const { result } = renderHook(() => usePullRequests(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current).toEqual({ + error: undefined, + items: pullRequests, + loading: false, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => usePullRequests(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => usePullRequests(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts index c412836ae5..c4223871f2 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.ts +++ b/plugins/azure-devops/src/hooks/usePullRequests.ts @@ -47,10 +47,7 @@ export function usePullRequests( const { project, repo } = getAnnotationValuesFromEntity(entity); const { value, loading, error } = useAsync(async () => { - if (repo) { - return await api.getPullRequests(project, repo, options); - } - return undefined; + return await api.getPullRequests(project, repo as string, options); }, [api, project, repo, top, status]); return { diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx b/plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx new file mode 100644 index 0000000000..99eaeb1ce3 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; +import { RepoBuild } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; +import { useRepoBuilds } from './useRepoBuilds'; + +describe('useRepoBuilds', () => { + const azureDevOpsApiMock = { + getRepoBuilds: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide an array of RepoBuild', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const repoBuilds: RepoBuild[] = [ + { + id: 1, + title: 'title-1', + source: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + id: 2, + title: 'title-2', + source: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + id: 3, + title: 'title-3', + source: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + ]; + azureDevOpsApiMock.getRepoBuilds.mockResolvedValue({ + items: repoBuilds, + }); + const { result } = renderHook(() => useRepoBuilds(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current).toEqual({ + error: undefined, + items: repoBuilds, + loading: false, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => useRepoBuilds(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => useRepoBuilds(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts index 4ba88bd5fb..9292a6e035 100644 --- a/plugins/azure-devops/src/hooks/useRepoBuilds.ts +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -43,10 +43,7 @@ export function useRepoBuilds( const { project, repo } = getAnnotationValuesFromEntity(entity); const { value, loading, error } = useAsync(async () => { - if (repo) { - return await api.getRepoBuilds(project, repo, options); - } - return undefined; + return await api.getRepoBuilds(project, repo as string, options); }, [api, project, repo, entity]); return { diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index a5d3725716..67e46cee7b 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -157,7 +157,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/build-definition was not found', + 'Value for annotation "dev.azure.com/build-definition" was not found', ); }); }); @@ -180,7 +180,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/project was not found', + 'Value for annotation "dev.azure.com/project" was not found', ); }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 99b194a834..9feec8bf95 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -46,7 +46,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION]; if (!project) { throw new Error( - `Value for annotation ${AZURE_DEVOPS_PROJECT_ANNOTATION} was not found`, + `Value for annotation "${AZURE_DEVOPS_PROJECT_ANNOTATION}" was not found`, ); } @@ -54,7 +54,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION]; if (!definition) { throw new Error( - `Value for annotation ${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION} was not found`, + `Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`, ); } return { project, definition, host, org }; From c0bb5117955f0f203d5be00e31462042c5bfb75b Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 21 Oct 2023 15:18:30 -0500 Subject: [PATCH 52/70] Refactor Readme into hook Signed-off-by: Andre Wanlin --- .../src/components/ReadmeCard/ReadmeCard.tsx | 15 +-- plugins/azure-devops/src/hooks/index.ts | 1 + .../azure-devops/src/hooks/useReadme.test.tsx | 108 ++++++++++++++++++ plugins/azure-devops/src/hooks/useReadme.ts | 42 +++++++ 4 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 plugins/azure-devops/src/hooks/useReadme.test.tsx create mode 100644 plugins/azure-devops/src/hooks/useReadme.ts diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index d9afe8db5a..420fe6ecf4 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -23,11 +23,9 @@ import { ErrorPanel, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; -import { azureDevOpsApiRef } from '../../api'; -import useAsync from 'react-use/lib/useAsync'; -import { getAnnotationValuesFromEntity } from '../../utils'; + +import { useReadme } from '../../hooks'; const useStyles = makeStyles(theme => ({ readMe: { @@ -85,16 +83,9 @@ const ReadmeCardError = ({ error }: ErrorProps) => { export const ReadmeCard = (props: Props) => { const classes = useStyles(); - const api = useApi(azureDevOpsApiRef); const { entity } = useEntity(); - const { project, repo } = getAnnotationValuesFromEntity(entity); - const { loading, error, value } = useAsync(async () => { - if (repo) { - return await api.getReadme({ project, repo }); - } - return undefined; - }, [api, project, repo, entity]); + const { loading, error, item: value } = useReadme(entity); if (loading) { return ; diff --git a/plugins/azure-devops/src/hooks/index.ts b/plugins/azure-devops/src/hooks/index.ts index 0b745ba586..ab1a3c9998 100644 --- a/plugins/azure-devops/src/hooks/index.ts +++ b/plugins/azure-devops/src/hooks/index.ts @@ -20,3 +20,4 @@ export * from './usePullRequests'; export * from './useRepoBuilds'; export * from './useUserEmail'; export * from './useUserTeamIds'; +export * from './useReadme'; diff --git a/plugins/azure-devops/src/hooks/useReadme.test.tsx b/plugins/azure-devops/src/hooks/useReadme.test.tsx new file mode 100644 index 0000000000..ffc33c136b --- /dev/null +++ b/plugins/azure-devops/src/hooks/useReadme.test.tsx @@ -0,0 +1,108 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; +import { Readme } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; +import { useReadme } from './useReadme'; + +describe('useReadme', () => { + const azureDevOpsApiMock = { + getReadme: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide a Readme', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const readme: Readme = { + url: 'https://dev.azure.com/org/project/repo', + content: 'This is some fake README content', + }; + azureDevOpsApiMock.getReadme.mockResolvedValue({ + item: readme, + }); + const { result } = renderHook(() => useReadme(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current.item).toEqual({ + item: readme, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => useReadme(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => useReadme(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/useReadme.ts b/plugins/azure-devops/src/hooks/useReadme.ts new file mode 100644 index 0000000000..60b2e21990 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useReadme.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Readme } from '@backstage/plugin-azure-devops-common'; + +import { Entity } from '@backstage/catalog-model'; +import { azureDevOpsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { getAnnotationValuesFromEntity } from '../utils'; + +export function useReadme(entity: Entity): { + item?: Readme; + loading: boolean; + error?: Error; +} { + const api = useApi(azureDevOpsApiRef); + const { project, repo } = getAnnotationValuesFromEntity(entity); + + const { value, loading, error } = useAsync(async () => { + return await api.getReadme({ project, repo: repo as string }); + }, [api, project, repo]); + + return { + item: value, + loading, + error, + }; +} From 8613ba3928e09d167511f84208afe51cc12d9edc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 21 Oct 2023 15:28:00 +0200 Subject: [PATCH 53/70] plugins: remove usages of --experimental-type-build Signed-off-by: Patrik Oldsberg --- .changeset/sour-toes-joke.md | 13 ++++++ .../02-adding-a-basic-permission-check.md | 2 +- packages/backend-next/src/index.ts | 2 +- plugins/bazaar-backend/alpha-api-report.md | 13 ++++++ plugins/bazaar-backend/api-report.md | 5 --- plugins/bazaar-backend/package.json | 22 +++++++--- .../src/{plugin.ts => alpha.ts} | 2 +- plugins/bazaar-backend/src/index.ts | 1 - .../example-todo-list-backend/api-report.md | 2 +- .../example-todo-list-backend/package.json | 5 +-- .../example-todo-list-backend/src/plugin.ts | 2 +- plugins/kafka-backend/alpha-api-report.md | 13 ++++++ plugins/kafka-backend/api-report.md | 5 --- plugins/kafka-backend/package.json | 22 +++++++--- .../kafka-backend/src/{plugin.ts => alpha.ts} | 2 +- plugins/kafka-backend/src/index.ts | 1 - plugins/periskop-backend/alpha-api-report.md | 13 ++++++ plugins/periskop-backend/api-report.md | 5 --- plugins/periskop-backend/package.json | 22 +++++++--- .../src/{plugin.ts => alpha.ts} | 2 +- plugins/periskop-backend/src/index.ts | 1 - plugins/proxy-backend/alpha-api-report.md | 13 ++++++ plugins/proxy-backend/api-report.md | 5 --- plugins/proxy-backend/package.json | 22 +++++++--- .../proxy-backend/src/{plugin.ts => alpha.ts} | 2 +- plugins/proxy-backend/src/index.ts | 1 - .../src/ScaffolderPlugin.ts | 8 ++-- plugins/scaffolder-node/alpha-api-report.md | 42 +++++++++++++++++++ plugins/scaffolder-node/api-report.md | 34 --------------- plugins/scaffolder-node/package.json | 22 +++++++--- .../src/{extensions.ts => alpha.ts} | 0 plugins/scaffolder-node/src/index.ts | 8 ---- plugins/sonarqube/package.json | 5 +-- plugins/todo/package.json | 5 +-- .../user-settings-backend/alpha-api-report.md | 13 ++++++ plugins/user-settings-backend/api-report.md | 5 --- plugins/user-settings-backend/package.json | 22 +++++++--- .../src/{plugin.ts => alpha.ts} | 2 +- plugins/user-settings-backend/src/index.ts | 1 - 39 files changed, 242 insertions(+), 123 deletions(-) create mode 100644 .changeset/sour-toes-joke.md create mode 100644 plugins/bazaar-backend/alpha-api-report.md rename plugins/bazaar-backend/src/{plugin.ts => alpha.ts} (96%) create mode 100644 plugins/kafka-backend/alpha-api-report.md rename plugins/kafka-backend/src/{plugin.ts => alpha.ts} (96%) create mode 100644 plugins/periskop-backend/alpha-api-report.md rename plugins/periskop-backend/src/{plugin.ts => alpha.ts} (96%) create mode 100644 plugins/proxy-backend/alpha-api-report.md rename plugins/proxy-backend/src/{plugin.ts => alpha.ts} (96%) create mode 100644 plugins/scaffolder-node/alpha-api-report.md rename plugins/scaffolder-node/src/{extensions.ts => alpha.ts} (100%) create mode 100644 plugins/user-settings-backend/alpha-api-report.md rename plugins/user-settings-backend/src/{plugin.ts => alpha.ts} (95%) diff --git a/.changeset/sour-toes-joke.md b/.changeset/sour-toes-joke.md new file mode 100644 index 0000000000..4e380f4ded --- /dev/null +++ b/.changeset/sour-toes-joke.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-user-settings-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-periskop-backend': patch +'@backstage/plugin-scaffolder-node': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-kafka-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-todo': patch +--- + +Switched to using `"exports"` field for `/alpha` subpath export. diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 839441e733..23bc4e84f1 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -349,7 +349,7 @@ import { createRouter } from './service/router'; /** * The example TODO list backend plugin. * -* @alpha +* @public */ export const exampleTodoListPlugin = createBackendPlugin({ pluginId: 'exampleTodoList', diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index bdfbc2c26b..dd2c6994f9 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -39,7 +39,7 @@ backend.add( import('@backstage/plugin-permission-backend-module-allow-all-policy'), ); backend.add(import('@backstage/plugin-permission-backend/alpha')); -backend.add(import('@backstage/plugin-proxy-backend')); +backend.add(import('@backstage/plugin-proxy-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); diff --git a/plugins/bazaar-backend/alpha-api-report.md b/plugins/bazaar-backend/alpha-api-report.md new file mode 100644 index 0000000000..7ef46f20ab --- /dev/null +++ b/plugins/bazaar-backend/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-bazaar-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/bazaar-backend/api-report.md b/plugins/bazaar-backend/api-report.md index d9243fd3af..1ee889c04a 100644 --- a/plugins/bazaar-backend/api-report.md +++ b/plugins/bazaar-backend/api-report.md @@ -3,17 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; -// @alpha -const bazaarPlugin: () => BackendFeature; -export default bazaarPlugin; - // @public (undocumented) export function createRouter(options: RouterOptions): Promise; diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 3fdfa32a7f..2a0c2c8f28 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -5,10 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -21,7 +33,7 @@ }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/bazaar-backend/src/plugin.ts b/plugins/bazaar-backend/src/alpha.ts similarity index 96% rename from plugins/bazaar-backend/src/plugin.ts rename to plugins/bazaar-backend/src/alpha.ts index 1c06bda705..466181e073 100644 --- a/plugins/bazaar-backend/src/plugin.ts +++ b/plugins/bazaar-backend/src/alpha.ts @@ -26,7 +26,7 @@ import { createRouter } from './service/router'; * * @alpha */ -export const bazaarPlugin = createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'bazaar', register(env) { env.registerInit({ diff --git a/plugins/bazaar-backend/src/index.ts b/plugins/bazaar-backend/src/index.ts index d6f3df2d04..ca73cb27ba 100644 --- a/plugins/bazaar-backend/src/index.ts +++ b/plugins/bazaar-backend/src/index.ts @@ -15,4 +15,3 @@ */ export * from './service/router'; -export { bazaarPlugin as default } from './plugin'; diff --git a/plugins/example-todo-list-backend/api-report.md b/plugins/example-todo-list-backend/api-report.md index d78f5e0796..4c27733124 100644 --- a/plugins/example-todo-list-backend/api-report.md +++ b/plugins/example-todo-list-backend/api-report.md @@ -11,7 +11,7 @@ import { Logger } from 'winston'; // @public export function createRouter(options: RouterOptions): Promise; -// @alpha +// @public const exampleTodoListPlugin: () => BackendFeature; export default exampleTodoListPlugin; diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index c13bb571e2..21759dffa2 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -17,12 +17,11 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "types": "dist/index.d.ts" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/example-todo-list-backend/src/plugin.ts b/plugins/example-todo-list-backend/src/plugin.ts index 42985f6e88..6bdae201f1 100644 --- a/plugins/example-todo-list-backend/src/plugin.ts +++ b/plugins/example-todo-list-backend/src/plugin.ts @@ -24,7 +24,7 @@ import { createRouter } from './service/router'; /** * The example TODO list backend plugin. * - * @alpha + * @public */ export const exampleTodoListPlugin = createBackendPlugin({ pluginId: 'exampleTodoList', diff --git a/plugins/kafka-backend/alpha-api-report.md b/plugins/kafka-backend/alpha-api-report.md new file mode 100644 index 0000000000..01c035aa0d --- /dev/null +++ b/plugins/kafka-backend/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-kafka-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index b03bad12e5..20c8260b52 100644 --- a/plugins/kafka-backend/api-report.md +++ b/plugins/kafka-backend/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -11,10 +10,6 @@ import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; -// @alpha -const kafkaPlugin: () => BackendFeature; -export default kafkaPlugin; - // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 68060f254c..8533f54b35 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -6,10 +6,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -27,7 +39,7 @@ "configSchema": "config.d.ts", "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/kafka-backend/src/plugin.ts b/plugins/kafka-backend/src/alpha.ts similarity index 96% rename from plugins/kafka-backend/src/plugin.ts rename to plugins/kafka-backend/src/alpha.ts index 0548a5f128..bc627eb9d3 100644 --- a/plugins/kafka-backend/src/plugin.ts +++ b/plugins/kafka-backend/src/alpha.ts @@ -26,7 +26,7 @@ import { createRouter } from './service/router'; * * @alpha */ -export const kafkaPlugin = createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'kafka', register(env) { env.registerInit({ diff --git a/plugins/kafka-backend/src/index.ts b/plugins/kafka-backend/src/index.ts index d87d313941..70b7fb45a4 100644 --- a/plugins/kafka-backend/src/index.ts +++ b/plugins/kafka-backend/src/index.ts @@ -22,4 +22,3 @@ export type { RouterOptions } from './service/router'; export { createRouter } from './service/router'; -export { kafkaPlugin as default } from './plugin'; diff --git a/plugins/periskop-backend/alpha-api-report.md b/plugins/periskop-backend/alpha-api-report.md new file mode 100644 index 0000000000..d6ace05993 --- /dev/null +++ b/plugins/periskop-backend/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-periskop-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/periskop-backend/api-report.md b/plugins/periskop-backend/api-report.md index 46d5de8d93..caaed109f9 100644 --- a/plugins/periskop-backend/api-report.md +++ b/plugins/periskop-backend/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -11,10 +10,6 @@ import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; -// @alpha -const periskopPlugin: () => BackendFeature; -export default periskopPlugin; - // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 1ef6c62431..73977e626d 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -14,14 +14,26 @@ "directory": "plugins/periskop-backend" }, "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/periskop-backend/src/plugin.ts b/plugins/periskop-backend/src/alpha.ts similarity index 96% rename from plugins/periskop-backend/src/plugin.ts rename to plugins/periskop-backend/src/alpha.ts index 2123dcbece..59271e02fc 100644 --- a/plugins/periskop-backend/src/plugin.ts +++ b/plugins/periskop-backend/src/alpha.ts @@ -26,7 +26,7 @@ import { createRouter } from './service/router'; * * @alpha */ -export const periskopPlugin = createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'periskop', register(env) { env.registerInit({ diff --git a/plugins/periskop-backend/src/index.ts b/plugins/periskop-backend/src/index.ts index 17a6d0d054..ca73cb27ba 100644 --- a/plugins/periskop-backend/src/index.ts +++ b/plugins/periskop-backend/src/index.ts @@ -15,4 +15,3 @@ */ export * from './service/router'; -export { periskopPlugin as default } from './plugin'; diff --git a/plugins/proxy-backend/alpha-api-report.md b/plugins/proxy-backend/alpha-api-report.md new file mode 100644 index 0000000000..73c3052acb --- /dev/null +++ b/plugins/proxy-backend/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-proxy-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index ae327d485a..12e6bfa1c9 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -12,10 +11,6 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public export function createRouter(options: RouterOptions): Promise; -// @alpha -const proxyPlugin: () => BackendFeature; -export default proxyPlugin; - // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index adbfe9fce9..4506f62107 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -6,10 +6,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -25,7 +37,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/proxy-backend/src/plugin.ts b/plugins/proxy-backend/src/alpha.ts similarity index 96% rename from plugins/proxy-backend/src/plugin.ts rename to plugins/proxy-backend/src/alpha.ts index e4c14b0037..9f6bad4832 100644 --- a/plugins/proxy-backend/src/plugin.ts +++ b/plugins/proxy-backend/src/alpha.ts @@ -26,7 +26,7 @@ import { createRouter } from './service/router'; * * @alpha */ -export const proxyPlugin = createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'proxy', register(env) { env.registerInit({ diff --git a/plugins/proxy-backend/src/index.ts b/plugins/proxy-backend/src/index.ts index 3163abd381..8f5d5c79dc 100644 --- a/plugins/proxy-backend/src/index.ts +++ b/plugins/proxy-backend/src/index.ts @@ -21,4 +21,3 @@ */ export * from './service'; -export { proxyPlugin as default } from './plugin'; diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 98bd4a1d66..beabaa50aa 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -22,14 +22,16 @@ import { loggerToWinstonLogger } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; import { - scaffolderActionsExtensionPoint, - scaffolderTaskBrokerExtensionPoint, - scaffolderTemplatingExtensionPoint, TaskBroker, TemplateAction, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; +import { + scaffolderActionsExtensionPoint, + scaffolderTaskBrokerExtensionPoint, + scaffolderTemplatingExtensionPoint, +} from '@backstage/plugin-scaffolder-node/alpha'; import { createBuiltinActions } from './scaffolder'; import { createRouter } from './service/router'; diff --git a/plugins/scaffolder-node/alpha-api-report.md b/plugins/scaffolder-node/alpha-api-report.md new file mode 100644 index 0000000000..c2a2a1597e --- /dev/null +++ b/plugins/scaffolder-node/alpha-api-report.md @@ -0,0 +1,42 @@ +## API Report File for "@backstage/plugin-scaffolder-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { TaskBroker } from '@backstage/plugin-scaffolder-node'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { TemplateFilter } from '@backstage/plugin-scaffolder-node'; +import { TemplateGlobal } from '@backstage/plugin-scaffolder-node'; + +// @alpha +export interface ScaffolderActionsExtensionPoint { + // (undocumented) + addActions(...actions: TemplateAction[]): void; +} + +// @alpha +export const scaffolderActionsExtensionPoint: ExtensionPoint; + +// @alpha +export interface ScaffolderTaskBrokerExtensionPoint { + // (undocumented) + setTaskBroker(taskBroker: TaskBroker): void; +} + +// @alpha +export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint; + +// @alpha +export interface ScaffolderTemplatingExtensionPoint { + // (undocumented) + addTemplateFilters(filters: Record): void; + // (undocumented) + addTemplateGlobals(filters: Record): void; +} + +// @alpha +export const scaffolderTemplatingExtensionPoint: ExtensionPoint; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 52ce3c8d13..08caf36812 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -5,7 +5,6 @@ ```ts /// -import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; @@ -13,11 +12,7 @@ import { Observable } from '@backstage/types'; import { Schema } from 'jsonschema'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; -import { TaskBroker as TaskBroker_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; -import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node'; -import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { UserEntity } from '@backstage/catalog-model'; @@ -109,35 +104,6 @@ export function fetchFile(options: { outputPath: string; }): Promise; -// @alpha -export interface ScaffolderActionsExtensionPoint { - // (undocumented) - addActions(...actions: TemplateAction_2[]): void; -} - -// @alpha -export const scaffolderActionsExtensionPoint: ExtensionPoint; - -// @alpha -export interface ScaffolderTaskBrokerExtensionPoint { - // (undocumented) - setTaskBroker(taskBroker: TaskBroker_2): void; -} - -// @alpha -export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint; - -// @alpha -export interface ScaffolderTemplatingExtensionPoint { - // (undocumented) - addTemplateFilters(filters: Record): void; - // (undocumented) - addTemplateGlobals(filters: Record): void; -} - -// @alpha -export const scaffolderTemplatingExtensionPoint: ExtensionPoint; - // @public export type SerializedTask = { id: string; diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 3194f0c0d6..8e887b480b 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -6,10 +6,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "alphaTypes": "dist/index.alpha.d.ts", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "node-library" @@ -22,7 +34,7 @@ }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", diff --git a/plugins/scaffolder-node/src/extensions.ts b/plugins/scaffolder-node/src/alpha.ts similarity index 100% rename from plugins/scaffolder-node/src/extensions.ts rename to plugins/scaffolder-node/src/alpha.ts diff --git a/plugins/scaffolder-node/src/index.ts b/plugins/scaffolder-node/src/index.ts index dcce065108..98691c2afa 100644 --- a/plugins/scaffolder-node/src/index.ts +++ b/plugins/scaffolder-node/src/index.ts @@ -23,11 +23,3 @@ export * from './actions'; export * from './tasks'; export type { TemplateFilter, TemplateGlobal } from './types'; -export { - scaffolderActionsExtensionPoint, - type ScaffolderActionsExtensionPoint, - scaffolderTaskBrokerExtensionPoint, - type ScaffolderTaskBrokerExtensionPoint, - scaffolderTemplatingExtensionPoint, - type ScaffolderTemplatingExtensionPoint, -} from './extensions'; diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 00d6c494db..4244c74759 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -8,8 +8,7 @@ "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "types": "dist/index.d.ts" }, "backstage": { "role": "frontend-plugin" @@ -27,7 +26,7 @@ ], "sideEffects": false, "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "start": "backstage-cli package start", "lint": "backstage-cli package lint", "test": "backstage-cli package test", diff --git a/plugins/todo/package.json b/plugins/todo/package.json index ec27528d72..6c230d7a4b 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -8,8 +8,7 @@ "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "types": "dist/index.d.ts" }, "backstage": { "role": "frontend-plugin" @@ -22,7 +21,7 @@ }, "sideEffects": false, "scripts": { - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "start": "backstage-cli package start", "lint": "backstage-cli package lint", "test": "backstage-cli package test", diff --git a/plugins/user-settings-backend/alpha-api-report.md b/plugins/user-settings-backend/alpha-api-report.md new file mode 100644 index 0000000000..aa6b246616 --- /dev/null +++ b/plugins/user-settings-backend/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-user-settings-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/user-settings-backend/api-report.md b/plugins/user-settings-backend/api-report.md index 808bd0f29b..1bc37bbec7 100644 --- a/plugins/user-settings-backend/api-report.md +++ b/plugins/user-settings-backend/api-report.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -19,9 +18,5 @@ export interface RouterOptions { identity: IdentityApi; } -// @alpha -const userSettingsPlugin: () => BackendFeature; -export default userSettingsPlugin; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index c72f349301..cf68c43c3b 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -9,10 +9,22 @@ "role": "backend-plugin" }, "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "homepage": "https://backstage.io", "repository": { @@ -22,7 +34,7 @@ }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build --experimental-type-build", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", diff --git a/plugins/user-settings-backend/src/plugin.ts b/plugins/user-settings-backend/src/alpha.ts similarity index 95% rename from plugins/user-settings-backend/src/plugin.ts rename to plugins/user-settings-backend/src/alpha.ts index e200d22ffe..8404c4ae94 100644 --- a/plugins/user-settings-backend/src/plugin.ts +++ b/plugins/user-settings-backend/src/alpha.ts @@ -25,7 +25,7 @@ import { createRouter } from './service/router'; * * @alpha */ -export const userSettingsPlugin = createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'userSettings', register(env) { env.registerInit({ diff --git a/plugins/user-settings-backend/src/index.ts b/plugins/user-settings-backend/src/index.ts index 14a9704e53..04d8accdaf 100644 --- a/plugins/user-settings-backend/src/index.ts +++ b/plugins/user-settings-backend/src/index.ts @@ -16,4 +16,3 @@ export * from './service'; export * from './database'; -export { userSettingsPlugin as default } from './plugin'; From 4e36abef145b41c2273b2aea0d5fcad7ff0e2acd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 21 Oct 2023 15:31:16 +0200 Subject: [PATCH 54/70] cli: remove support for --experimental-type-build Signed-off-by: Patrik Oldsberg --- .changeset/many-masks-smoke.md | 6 + REVIEWING.md | 8 +- docs/local-dev/cli-build-system.md | 35 ----- packages/cli/cli-report.md | 1 - packages/cli/package.json | 4 - packages/cli/src/commands/build/command.ts | 1 - packages/cli/src/commands/index.ts | 4 - .../src/commands/migrate/packageScripts.ts | 3 - packages/cli/src/commands/repo/build.ts | 1 - .../src/lib/builder/buildTypeDefinitions.ts | 129 ------------------ .../lib/builder/buildTypeDefinitionsWorker.ts | 97 ------------- packages/cli/src/lib/builder/config.ts | 2 +- packages/cli/src/lib/builder/packager.ts | 17 --- packages/cli/src/lib/builder/types.ts | 1 - .../src/lib/packager/createDistWorkspace.ts | 1 - .../src/commands/api-reports/api-extractor.ts | 11 +- yarn.lock | 3 - 17 files changed, 9 insertions(+), 315 deletions(-) create mode 100644 .changeset/many-masks-smoke.md delete mode 100644 packages/cli/src/lib/builder/buildTypeDefinitions.ts delete mode 100644 packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts diff --git a/.changeset/many-masks-smoke.md b/.changeset/many-masks-smoke.md new file mode 100644 index 0000000000..6d249eeb15 --- /dev/null +++ b/.changeset/many-masks-smoke.md @@ -0,0 +1,6 @@ +--- +'@backstage/repo-tools': minor +'@backstage/cli': minor +--- + +Remove support for the deprecated `--experimental-type-build` option for `package build`. diff --git a/REVIEWING.md b/REVIEWING.md index 43085090a8..7ed3fcda57 100644 --- a/REVIEWING.md +++ b/REVIEWING.md @@ -172,13 +172,7 @@ We generate API Reports using the [API Extractor](https://api-extractor.com/) to Each API report contains a list of all the exported types of each package. As long as the API report does not have any warnings it will contain the full publicly facing API of the package, meaning you do not need to consider any other changes to the package from the point of view of TypeScript API stability. -Exported types can be marked with either `@public`, `@alpha` or `@beta` release tags. It is only the `@public` exports that we consider to be part of the stable API. The `@alpha` and `@beta` exports are considered unstable and can be changed at any time without needing a breaking package versions bump. However, this **ONLY** applies if the package has been configured to use experimental type builds, which looks like this in `package.json`: - -```json - "build": "backstage-cli package build --experimental-type-build" -``` - -If a package does not have this configuration, then all exported types are considered stable, even if they are marked as `@alpha` or `@beta`. +Exported types can be marked with either `@public`, `@alpha` or `@beta` release tags. It is only the `@public` exports that we consider to be part of the stable API. The `@alpha` and `@beta` exports are considered unstable and can be changed at any time without needing a breaking package versions bump. #### Changes that are Not Considered Breaking diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index 61e24863fe..7bb4918d47 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -674,38 +674,3 @@ To add subpath exports to an existing package, simply add the desired `"exports" ```bash yarn backstage-cli migrate package-exports ``` - -## Experimental Type Build - -> Note: Experimental type builds are deprecated and will be removed in the future. They have been replaced by [subpath exports](#subpath-exports). - -The Backstage CLI has an experimental feature where multiple different type definition files can be generated for different release stages. The release stages are marked in the [TSDoc](https://tsdoc.org/) for each individual export, using either `@public`, `@alpha`, or `@beta`. Rather than just building a single `index.d.ts` file, the build process will instead output `index.d.ts`, `index.beta.d.ts`, and `index.alpha.d.ts`. Each of these files will have exports from more unstable release stages stripped, meaning that `index.d.ts` will omit all exports marked with `@alpha` or `@beta`, while `index.beta.d.ts` will omit all exports marked with `@alpha`. - -This feature is aimed at projects that publish to package registries and wish to maintain different levels of API stability within each package. There is no need to use this within a single monorepo, as it has no effect due to only applying to built and published packages. - -In order for the experimental type build to work, `@microsoft/api-extractor` must be installed in your project, as it is an optional peer dependency of the Backstage CLI. There are then three steps that need to be taken for each package where you want to enable this feature: - -- Add the `--experimental-type-build` flag to the `"build"` script of the package. -- Add either one or both of `"alphaTypes"` and `"betaTypes"` to the `"publishConfig"` of the package: - ```json - "publishConfig": { - ... - "types": "dist/index.d.ts", - "alphaTypes": "dist/index.alpha.d.ts", - "betaTypes": "dist/index.beta.d.ts" - }, - ``` -- Add either one or both of `"alpha"` and `"beta"` to the `"files"` of the package: - ```json - "files": [ - "dist", - "alpha", - "beta" - ] - ``` - -Once this setup is complete, users of the published packages will only be able to access the stable API via the main package entry point, for example `@acme/my-plugin`. Exports marked with `@alpha` or `@beta` will only be available via the `/alpha` entry point, for example `@acme/my-plugin/alpha`, and exports marked with `@beta` will only be available via `/beta`. This does not apply within the monorepo that contains the package. There all exports still have to be imported via the main entry point. - -Note that these different entry points are only separated during type checking. At runtime they all share the same code which contains the exports from all releases stages. - -An example of this setup can be seen in the [`@backstage/catalog-model`](https://github.com/backstage/backstage/blob/da0675bf9f28ed1460f03635a22d3c26abd14707/packages/catalog-model/package.json#L14) package, which has enabled `alpha` type exports. With this setup, exports marked as `@alpha` are only available for import via `@backstage/catalog-model/alpha`. The `@backstage/catalog-model` package currently does not have any exports marked as `@beta`, or a `/beta` entry point. diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 9d6ffd3f69..9292f2ea37 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -224,7 +224,6 @@ Usage: backstage-cli package build [options] Options: --role --minify - --experimental-type-build --skip-build-dependencies --stats --config diff --git a/packages/cli/package.json b/packages/cli/package.json index 4f08231f7e..5fb892952f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -175,16 +175,12 @@ "type-fest": "^2.19.0" }, "peerDependencies": { - "@microsoft/api-extractor": "^7.21.2", "@vitejs/plugin-react": "^4.0.4", "vite": "^4.4.9", "vite-plugin-html": "^3.2.0", "vite-plugin-node-polyfills": "^0.14.1" }, "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, "@vitejs/plugin-react": { "optional": true }, diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 507085e0a0..1b3aeff2f9 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -65,6 +65,5 @@ export async function command(opts: OptionValues): Promise { return buildPackage({ outputs, minify: Boolean(opts.minify), - useApiExtractor: Boolean(opts.experimentalTypeBuild), }); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ce4a4d2bae..fb0a55f37e 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -130,10 +130,6 @@ export function registerScriptCommand(program: Command) { '--minify', 'Minify the generated code. Does not apply to app or backend packages.', ) - .option( - '--experimental-type-build', - 'Enable experimental type build. Does not apply to app or backend packages. [DEPRECATED]', - ) .option( '--skip-build-dependencies', 'Skip the automatic building of local dependencies. Applies to backend packages only.', diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 1552208a5f..e9ff20aed3 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -49,9 +49,6 @@ export async function command() { if (scripts.build?.includes('--minify')) { buildCmd.push('--minify'); } - if (scripts.build?.includes('--experimental-type-build')) { - buildCmd.push('--experimental-type-build'); - } if (scripts.build?.includes('--config')) { buildCmd.push(...(scripts.build.match(configArgPattern) ?? [])); } diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 71e58c6201..32f227c208 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -137,7 +137,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise { outputs, logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, minify: buildOptions.minify, - useApiExtractor: buildOptions.experimentalTypeBuild, }; }); diff --git a/packages/cli/src/lib/builder/buildTypeDefinitions.ts b/packages/cli/src/lib/builder/buildTypeDefinitions.ts deleted file mode 100644 index 3bf3a4495f..0000000000 --- a/packages/cli/src/lib/builder/buildTypeDefinitions.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import chalk from 'chalk'; -import { relative as relativePath, resolve as resolvePath } from 'path'; -import { paths } from '../paths'; -import { buildTypeDefinitionsWorker } from './buildTypeDefinitionsWorker'; -import { runWorkerThreads } from '../parallel'; - -// These message types are ignored since we want to avoid duplicating the logic of -// handling them correctly, and we already have the API Reports warning about them. -const ignoredMessages = new Set(['tsdoc-undefined-tag', 'ae-forgotten-export']); - -export async function buildTypeDefinitions( - targetDirs: string[] = [paths.targetDir], -) { - const packageDirs = targetDirs.map(dir => - relativePath(paths.targetRoot, dir), - ); - const entryPoints = await Promise.all( - packageDirs.map(async dir => { - const entryPoint = paths.resolveTargetRoot( - 'dist-types', - dir, - 'src/index.d.ts', - ); - - const declarationsExist = await fs.pathExists(entryPoint); - if (!declarationsExist) { - throw new Error( - `No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white( - 'yarn tsc', - )} to generate .d.ts files before packaging`, - ); - } - return entryPoint; - }), - ); - - const workerConfigs = packageDirs.map(packageDir => { - const targetDir = paths.resolveTargetRoot(packageDir); - const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir); - const extractorOptions = { - configObject: { - mainEntryPointFilePath: resolvePath(targetTypesDir, 'src/index.d.ts'), - bundledPackages: [], - - compiler: { - skipLibCheck: true, - tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'), - }, - - dtsRollup: { - enabled: true, - untrimmedFilePath: resolvePath(targetDir, 'dist/index.alpha.d.ts'), - betaTrimmedFilePath: resolvePath(targetDir, 'dist/index.beta.d.ts'), - publicTrimmedFilePath: resolvePath(targetDir, 'dist/index.d.ts'), - }, - - newlineKind: 'lf', - - projectFolder: targetDir, - }, - configObjectFullPath: targetDir, - packageJsonFullPath: resolvePath(targetDir, 'package.json'), - }; - return { extractorOptions, targetTypesDir }; - }); - - const typescriptDir = paths.resolveTargetRoot('node_modules/typescript'); - const hasTypescript = await fs.pathExists(typescriptDir); - const typescriptCompilerFolder = hasTypescript ? typescriptDir : undefined; - await runWorkerThreads({ - threadCount: 1, - workerData: { - entryPoints, - workerConfigs, - typescriptCompilerFolder, - }, - worker: buildTypeDefinitionsWorker, - onMessage: ({ - message, - targetTypesDir, - }: { - message: any; - targetTypesDir: string; - }) => { - if (ignoredMessages.has(message.messageId)) { - return; - } - - let text = `${message.text} (${message.messageId})`; - if (message.sourceFilePath) { - text += ' at '; - text += relativePath(targetTypesDir, message.sourceFilePath); - if (message.sourceFileLine) { - text += `:${message.sourceFileLine}`; - if (message.sourceFileColumn) { - text += `:${message.sourceFileColumn}`; - } - } - } - if (message.logLevel === 'error') { - console.error(chalk.red(`Error: ${text}`)); - } else if ( - message.logLevel === 'warning' || - message.category === 'Extractor' - ) { - console.warn(`Warning: ${text}`); - } else { - console.log(text); - } - }, - }); -} diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts deleted file mode 100644 index c3c37d91cf..0000000000 --- a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2022 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. - */ - -/** - * NOTE: This is a worker thread function that is stringified and executed - * within a `worker_threads.Worker`. Everything in this function must - * be self-contained. - * Using TypeScript is fine as it is transpiled before being stringified. - */ -export async function buildTypeDefinitionsWorker( - workerData: any, - sendMessage: (message: any) => void, -) { - try { - require('@microsoft/api-extractor'); - } catch (error) { - throw new Error( - 'Failed to resolve @microsoft/api-extractor, it must best installed ' + - 'as a dependency of your project in order to use experimental type builds', - ); - } - - const { dirname } = require('path'); - const { entryPoints, workerConfigs, typescriptCompilerFolder } = workerData; - - const apiExtractor = require('@microsoft/api-extractor'); - const { Extractor, ExtractorConfig, CompilerState } = apiExtractor; - - /** - * All of this monkey patching below is because Material UI has these bare package.json file as a method - * for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking - * by declaring them side effect free. - * - * The package.json lookup logic in api-extractor really doesn't like that though, as it enforces - * that the 'name' field exists in all package.json files that it discovers. This below is just - * making sure that we ignore those file package.json files instead of crashing. - */ - const { - PackageJsonLookup, - // eslint-disable-next-line @backstage/no-undeclared-imports - } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); - - const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; - PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = - function tryGetPackageJsonFilePathForPatch(path: string) { - if ( - path.includes('@material-ui') && - !dirname(path).endsWith('@material-ui') - ) { - return undefined; - } - return old.call(this, path); - }; - - let compilerState; - for (const { extractorOptions, targetTypesDir } of workerConfigs) { - const extractorConfig = ExtractorConfig.prepare(extractorOptions); - - if (!compilerState) { - compilerState = CompilerState.create(extractorConfig, { - additionalEntryPoints: entryPoints, - }); - } - - const extractorResult = Extractor.invoke(extractorConfig, { - compilerState, - localBuild: false, - typescriptCompilerFolder, - showVerboseMessages: false, - showDiagnostics: false, - messageCallback: (message: any) => { - message.handled = true; - sendMessage({ message, targetTypesDir }); - }, - }); - - if (!extractorResult.succeeded) { - throw new Error( - `Type definition build completed with ${extractorResult.errorCount} errors` + - ` and ${extractorResult.warningCount} warnings`, - ); - } - } -} diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index f614581320..41e9dd6906 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -151,7 +151,7 @@ export async function makeRollupConfigs( }); } - if (options.outputs.has(Output.types) && !options.useApiExtractor) { + if (options.outputs.has(Output.types)) { const input = Object.fromEntries( scriptEntryPoints.map(e => [ e.name, diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index 780f9c2cfe..be9bc9a6ea 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -21,7 +21,6 @@ import { relative as relativePath, resolve as resolvePath } from 'path'; import { paths } from '../paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; -import { buildTypeDefinitions } from './buildTypeDefinitions'; import { PackageRoles } from '@backstage/cli-node'; import { runParallelWorkers } from '../parallel'; @@ -112,10 +111,6 @@ export const buildPackage = async (options: BuildOptions) => { const buildTasks = rollupConfigs.map(rollupBuild); - if (options.outputs.has(Output.types) && options.useApiExtractor) { - buildTasks.push(buildTypeDefinitions()); - } - await Promise.all(buildTasks); }; @@ -131,18 +126,6 @@ export const buildPackages = async (options: BuildOptions[]) => { const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts)); - const typeDefinitionTargetDirs = options - .filter( - ({ outputs, useApiExtractor }) => - outputs.has(Output.types) && useApiExtractor, - ) - .map(_ => _.targetDir!); - - if (typeDefinitionTargetDirs.length > 0) { - // Make sure this one is started first - buildTasks.unshift(() => buildTypeDefinitions(typeDefinitionTargetDirs)); - } - await runParallelWorkers({ items: buildTasks, worker: async task => task(), diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index 330419235f..7d9c34fe29 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -28,5 +28,4 @@ export type BuildOptions = { packageJson?: BackstagePackageJson; outputs: Set; minify?: boolean; - useApiExtractor?: boolean; }; diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index e91924dd37..00b8784d6d 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -206,7 +206,6 @@ export async function createDistWorkspace( logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, // No need to detect these for the backend builds, we assume no minification or types minify: false, - useApiExtractor: false, }); } } diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index 13b6acb8fd..ce3e6f5b20 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -304,7 +304,6 @@ async function findPackageEntryPoints(packageDirs: string[]): Promise< Array<{ packageDir: string; name: string; - usesExperimentalTypeBuild?: boolean; }> > { return Promise.all( @@ -317,9 +316,6 @@ async function findPackageEntryPoints(packageDirs: string[]): Promise< getPackageExportNames(pkg)?.map(name => ({ packageDir, name })) ?? { packageDir, name: 'index', - usesExperimentalTypeBuild: pkg.scripts?.build?.includes( - '--experimental-type-build', - ), } ); }), @@ -367,11 +363,7 @@ export async function runApiExtraction({ } const warnings = new Array(); - for (const { - packageDir, - name, - usesExperimentalTypeBuild, - } of packageEntryPoints) { + for (const { packageDir, name } of packageEntryPoints) { console.log(`## Processing ${packageDir}`); const noBail = Array.isArray(allowWarnings) ? allowWarnings.some(aw => aw === packageDir || minimatch(packageDir, aw)) @@ -511,7 +503,6 @@ export async function runApiExtraction({ // The root index entrypoint is only allowed @public exports, while /alpha and /beta only allow @alpha and @beta. if ( validateReleaseTags && - !usesExperimentalTypeBuild && fs.pathExistsSync(extractorConfig.reportFilePath) ) { if (['index', 'alpha', 'beta'].includes(name)) { diff --git a/yarn.lock b/yarn.lock index 0242c61e33..13934f039a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3870,14 +3870,11 @@ __metadata: yn: ^4.0.0 zod: ^3.21.4 peerDependencies: - "@microsoft/api-extractor": ^7.21.2 "@vitejs/plugin-react": ^4.0.4 vite: ^4.4.9 vite-plugin-html: ^3.2.0 vite-plugin-node-polyfills: ^0.14.1 peerDependenciesMeta: - "@microsoft/api-extractor": - optional: true "@vitejs/plugin-react": optional: true vite: From 8db5c3cd7a6cdf66cea2707c2b29336fb5036287 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Oct 2023 11:01:13 +0200 Subject: [PATCH 55/70] cli,cli-node: remove support for alphaTypes/betaTypes Signed-off-by: Patrik Oldsberg --- .changeset/chilly-books-sneeze.md | 6 +++ packages/cli-node/api-report.md | 2 - .../cli-node/src/monorepo/PackageGraph.ts | 2 - .../cli/src/lib/packager/productionPack.ts | 48 +------------------ 4 files changed, 7 insertions(+), 51 deletions(-) create mode 100644 .changeset/chilly-books-sneeze.md diff --git a/.changeset/chilly-books-sneeze.md b/.changeset/chilly-books-sneeze.md new file mode 100644 index 0000000000..a2d3ca1e04 --- /dev/null +++ b/.changeset/chilly-books-sneeze.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli-node': minor +'@backstage/cli': minor +--- + +Removed support for the `publishConfig.alphaTypes` and `.betaTypes` fields that were used together with `--experimental-type-build` to generate `/alpha` and `/beta` entry points. Use the `exports` field to achieve this instead. diff --git a/packages/cli-node/api-report.md b/packages/cli-node/api-report.md index 91148596b3..71be6c4bd4 100644 --- a/packages/cli-node/api-report.md +++ b/packages/cli-node/api-report.md @@ -53,8 +53,6 @@ export interface BackstagePackageJson { access?: 'public' | 'restricted'; directory?: string; registry?: string; - alphaTypes?: string; - betaTypes?: string; }; // (undocumented) scripts?: { diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 421c49f9b9..fb46dbde26 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -56,8 +56,6 @@ export interface BackstagePackageJson { access?: 'public' | 'restricted'; directory?: string; registry?: string; - alphaTypes?: string; - betaTypes?: string; }; dependencies?: { diff --git a/packages/cli/src/lib/packager/productionPack.ts b/packages/cli/src/lib/packager/productionPack.ts index aea5df649a..6a59924a30 100644 --- a/packages/cli/src/lib/packager/productionPack.ts +++ b/packages/cli/src/lib/packager/productionPack.ts @@ -23,7 +23,7 @@ import { readEntryPoints } from '../entryPoints'; const PKG_PATH = 'package.json'; const PKG_BACKUP_PATH = 'package.json-prepack'; -const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; +const SKIPPED_KEYS = ['access', 'registry', 'tag']; const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx']; interface ProductionPackOptions { @@ -42,14 +42,6 @@ export async function productionPack(options: ProductionPackOptions) { await fs.writeFile(PKG_BACKUP_PATH, pkgContent); } - const hasStageEntry = - !!pkg.publishConfig?.alphaTypes || !!pkg.publishConfig?.betaTypes; - if (pkg.exports && hasStageEntry) { - throw new Error( - 'Combining both exports and alpha/beta types is not supported', - ); - } - // This mutates pkg to fill in index exports, so call it before applying publishConfig const writeCompatibilityEntryPoints = await prepareExportsEntryPoints( pkg, @@ -99,12 +91,6 @@ export async function productionPack(options: ProductionPackOptions) { await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); } - if (publishConfig.alphaTypes) { - await writeReleaseStageEntrypoint(pkg, 'alpha', targetDir ?? packageDir); - } - if (publishConfig.betaTypes) { - await writeReleaseStageEntrypoint(pkg, 'beta', targetDir ?? packageDir); - } if (writeCompatibilityEntryPoints) { await writeCompatibilityEntryPoints(targetDir ?? packageDir); } @@ -118,12 +104,6 @@ export async function revertProductionPack(packageDir: string) { // Check if we're shipping types for other release stages, clean up in that case const pkg = await fs.readJson(PKG_PATH); - if (pkg.publishConfig?.alphaTypes) { - await fs.remove(resolvePath(packageDir, 'alpha')); - } - if (pkg.publishConfig?.betaTypes) { - await fs.remove(resolvePath(packageDir, 'beta')); - } // Remove any extra entrypoint backwards compatibility directories const entryPoints = readEntryPoints(pkg); @@ -140,32 +120,6 @@ export async function revertProductionPack(packageDir: string) { } } -function resolveEntrypoint(pkg: any, name: string) { - const targetEntry = pkg.publishConfig[name] || pkg[name]; - return targetEntry && posixPath.join('..', targetEntry); -} - -// Writes e.g. alpha/package.json -async function writeReleaseStageEntrypoint( - pkg: BackstagePackageJson, - stage: 'alpha' | 'beta', - targetDir: string, -) { - await fs.ensureDir(resolvePath(targetDir, stage)); - await fs.writeJson( - resolvePath(targetDir, stage, PKG_PATH), - { - name: pkg.name, - version: pkg.version, - main: resolveEntrypoint(pkg, 'main'), - module: resolveEntrypoint(pkg, 'module'), - browser: resolveEntrypoint(pkg, 'browser'), - types: posixPath.join('..', pkg.publishConfig![`${stage}Types`]!), - }, - { encoding: 'utf8', spaces: 2 }, - ); -} - const EXPORT_MAP = { import: '.esm.js', require: '.cjs.js', From d1c549ea9d090b751f9a56b3c7b96dd0bd722d46 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Mon, 23 Oct 2023 09:40:15 +0000 Subject: [PATCH 56/70] Pass update input to createPullRequest (fixes: #17911) Signed-off-by: Alex Eftimie --- .../actions/builtin/publish/githubPullRequest.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index a02f21cfc2..3b6e7c3ced 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -142,6 +142,7 @@ export const createPublishGithubPullRequestAction = ( reviewers?: string[]; teamReviewers?: string[]; commitMessage?: string; + update?: boolean; }>({ id: 'publish:github:pull-request', examples, @@ -219,6 +220,11 @@ export const createPublishGithubPullRequestAction = ( title: 'Commit Message', description: 'The commit message for the pull request commit', }, + update: { + type: 'boolean', + title: 'Update', + description: 'Update pull request if already exists', + }, }, }, output: { @@ -256,6 +262,7 @@ export const createPublishGithubPullRequestAction = ( reviewers, teamReviewers, commitMessage, + update, } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -327,6 +334,7 @@ export const createPublishGithubPullRequestAction = ( body: description, head: branchName, draft, + update, }; if (targetBranchName) { createOptions.base = targetBranchName; From 5e4127c18ee24b96995aa5dd1aad9ee74a733f23 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Mon, 23 Oct 2023 10:11:53 +0000 Subject: [PATCH 57/70] Add changeset Signed-off-by: Alex Eftimie --- .changeset/tall-colts-roll.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tall-colts-roll.md diff --git a/.changeset/tall-colts-roll.md b/.changeset/tall-colts-roll.md new file mode 100644 index 0000000000..914a09cdac --- /dev/null +++ b/.changeset/tall-colts-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Allow setting `update: true` in `publish:github:pull-request` scaffolder action From 96c4f54bf6070db12676e9af0bf75d0d479c3d72 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Oct 2023 11:23:43 +0200 Subject: [PATCH 58/70] auth-backend: revert microsoft auth implementation Signed-off-by: Patrik Oldsberg --- .changeset/strange-queens-deliver.md | 5 + plugins/auth-backend/api-report.md | 6 +- plugins/auth-backend/config.d.ts | 12 + plugins/auth-backend/package.json | 1 - .../microsoft/__testUtils__/fake.test.ts | 90 ++++ .../providers/microsoft/__testUtils__/fake.ts | 126 +++++ .../src/providers/microsoft/provider.test.ts | 450 ++++++++++++++++++ .../src/providers/microsoft/provider.ts | 303 ++++++++++-- yarn.lock | 3 +- 9 files changed, 962 insertions(+), 34 deletions(-) create mode 100644 .changeset/strange-queens-deliver.md create mode 100644 plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts create mode 100644 plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts create mode 100644 plugins/auth-backend/src/providers/microsoft/provider.test.ts diff --git a/.changeset/strange-queens-deliver.md b/.changeset/strange-queens-deliver.md new file mode 100644 index 0000000000..ed1d4d0b5b --- /dev/null +++ b/.changeset/strange-queens-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Reverted the Microsoft auth provider to the previous implementation. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 41536f2a3f..6372c2ee60 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -538,9 +538,9 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailMatchingUserEntityProfileEmail: () => SignInResolver_2; - emailLocalPartMatchingUserEntityName: () => SignInResolver_2; - emailMatchingUserEntityAnnotation: () => SignInResolver_2; + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; }>; }>; oauth2: Readonly<{ diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index f8953e1588..e719468ac1 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -179,6 +179,18 @@ export interface Config { }; }; /** @visibility frontend */ + microsoft?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + tenantId: string; + callbackUrl?: string; + }; + }; + /** @visibility frontend */ onelogin?: { [authEnv: string]: { clientId: string; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 61e7993ab3..6de6fddd58 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -42,7 +42,6 @@ "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^", "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", - "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^", "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", diff --git a/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts new file mode 100644 index 0000000000..dee37f0f6b --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { FakeMicrosoftAPI } from './fake'; + +describe('FakeMicrosoftAPI', () => { + const api = new FakeMicrosoftAPI(); + + describe('#token', () => { + it('exchanges auth codes', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('User.Read'), + }), + ); + + expect(api.tokenHasScope(access_token, 'User.Read')).toBe(true); + }); + + it('supports scopes for the first requested audience only', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('someaudience/somescope User.Read'), + }), + ); + + expect(api.tokenHasScope(access_token, 'User.Read')).toBe(false); + }); + + it('special openid scopes do not count towards the 1-audience limit', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('openid offline_access User.Read'), + }), + ); + + expect(api.tokenHasScope(access_token, 'User.Read')).toBe(true); + }); + + it('refreshes tokens', () => { + const { access_token } = api.token( + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: api.generateRefreshToken( + 'email openid profile User.Read', + ), + }), + ); + + expect( + api.tokenHasScope(access_token, 'email openid profile User.Read'), + ).toBe(true); + }); + it('requires `openid` scope for ID token', () => { + const { id_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('User.Read'), + }), + ); + + expect(id_token).toBeUndefined(); + }); + it('requires `offline_access` scope for refresh token', () => { + const { refresh_token } = api.token( + new URLSearchParams({ + grant_type: 'authorization_code', + code: api.generateAuthCode('User.Read'), + }), + ); + + expect(refresh_token).toBeUndefined(); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts new file mode 100644 index 0000000000..9b3ca09c57 --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/__testUtils__/fake.ts @@ -0,0 +1,126 @@ +/* + * 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 { decodeJwt } from 'jose'; + +type Claims = { aud: string; scp: string }; + +export class FakeMicrosoftAPI { + generateAccessToken(scope: string): string { + return this.tokenWithClaims(this.allClaimsForScope(scope)).access_token; + } + generateAuthCode(scope: string): string { + return this.encodeClaims(this.allClaimsForScope(scope)); + } + generateRefreshToken(scope: string): string { + return this.encodeClaims(this.allClaimsForScope(scope)); + } + token(formData: URLSearchParams): { + access_token: string; + scope: string; + refresh_token?: string; + id_token?: string; + } { + const scopeParameter = formData.get('scope'); + const claims = + (scopeParameter && this.allClaimsForScope(scopeParameter)) ?? + formData.get('grant_type') === 'refresh_token' + ? this.decodeClaims(formData.get('refresh_token')!) + : this.decodeClaims(formData.get('code')!); + return { + ...this.tokenWithClaims(claims), + ...(this.hasScope(claims, 'offline_access') && { + refresh_token: this.encodeClaims(claims), + }), + ...(this.hasScope(claims, 'openid') && { + id_token: 'header.e30K.microsoft', + }), + }; + } + tokenHasScope(token: string, scope: string): boolean { + const { aud, scp } = decodeJwt(token); + return this.hasScope({ aud: aud as string, scp: scp as string }, scope); + } + private tokenWithClaims(claims: Claims): { + access_token: string; + scope: string; + } { + const filteredClaims = { + ...claims, + scp: claims.scp + .split(' ') + .filter(s => s !== 'offline_access') + .join(' '), + }; + return { + access_token: `header.${Buffer.from( + JSON.stringify(filteredClaims), + ).toString('base64')}.signature`, + scope: this.scopeFromClaims(filteredClaims), + }; + } + private allClaimsForScope(scope: string): Claims { + const scopes = scope.split(' ').map(this.parseScope); + const firstAudience = scopes + .map(({ aud }) => aud) + .find(aud => aud !== 'openid'); + return { + aud: firstAudience ?? '00000003-0000-0000-c000-000000000000', + scp: scopes + .filter(({ aud }) => aud === 'openid' || aud === firstAudience) + .map(({ scp }) => scp) + .join(' '), + }; + } + // auth codes and refresh tokens in this fake system are base64-encoded JSON + // strings of claims + private encodeClaims(claims: Claims): string { + return Buffer.from(JSON.stringify(claims)).toString('base64'); + } + private decodeClaims(encoded: string): Claims { + return JSON.parse(Buffer.from(encoded, 'base64').toString()); + } + private hasScope(claims: Claims, scope: string): boolean { + return this.scopeFromClaims(claims).includes(scope); + } + private parseScope(s: string): Claims { + if (s.includes('/')) { + const [aud, scp] = s.split('/'); + return { aud, scp }; + } + switch (s) { + case 'email': + case 'openid': + case 'offline_access': + case 'profile': { + return { aud: 'openid', scp: s }; + } + default: + return { aud: '00000003-0000-0000-c000-000000000000', scp: s }; + } + } + private scopeFromClaims(claims: Claims): string { + return claims.scp + .split(' ') + .map(this.parseScope) + .map(({ aud, scp }) => + aud === 'openid' || + claims.aud === '00000003-0000-0000-c000-000000000000' + ? scp + : `${claims.aud}/${scp}`, + ) + .join(' '); + } +} diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts new file mode 100644 index 0000000000..7bbfd085d6 --- /dev/null +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -0,0 +1,450 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { microsoft } from './provider'; +import { getVoidLogger } from '@backstage/backend-common'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { AuthProviderRouteHandlers, AuthResolverContext } from '../types'; +import express from 'express'; +import crypto from 'crypto'; +import { FakeMicrosoftAPI } from './__testUtils__/fake'; + +describe('MicrosoftAuthProvider', () => { + const nonce = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 bytes of zeros in base64 + const state = Buffer.from( + `nonce=${encodeURIComponent(nonce)}&env=development`, + ).toString('hex'); + const mockBackstageToken = `header.${Buffer.from( + JSON.stringify({ sub: 'user:default/mock' }), + 'utf8', + ).toString('base64')}.backstage`; + + const server = setupServer(); + const microsoftApi = new FakeMicrosoftAPI(); + let provider: AuthProviderRouteHandlers; + let response: jest.Mocked; + + setupRequestMockHandlers(server); + + beforeEach(() => { + provider = microsoft.create({ + signIn: { + resolver: microsoft.resolvers.emailMatchingUserEntityAnnotation(), + }, + })({ + providerId: 'microsoft', + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, + globalConfig: { + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, + }, + config: new ConfigReader({ + development: { + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + }), + logger: getVoidLogger(), + resolverContext: { + issueToken: jest.fn(), + findCatalogUser: jest.fn(), + signInWithCatalogUser: async _ => ({ + token: mockBackstageToken, + }), + } as AuthResolverContext, + }) as AuthProviderRouteHandlers; + + server.use( + rest.post( + 'https://login.microsoftonline.com/tenantId/oauth2/v2.0/token', + async (req, res, ctx) => { + return res( + ctx.json({ + ...microsoftApi.token(new URLSearchParams(await req.text())), + token_type: 'Bearer', + expires_in: 123, + ext_expires_in: 123, + }), + ); + }, + ), + rest.get('https://graph.microsoft.com/v1.0/me/', (req, res, ctx) => { + if ( + !microsoftApi.tokenHasScope( + req.headers.get('authorization')!.replace(/^Bearer /, ''), + 'User.Read', + ) + ) { + return res(ctx.status(403)); + } + return res( + ctx.json({ + id: 'conrad', + displayName: 'Conrad', + surname: 'Ribas', + givenName: 'Francisco', + mail: 'conrad@example.com', + }), + ); + }), + rest.get( + 'https://graph.microsoft.com/v1.0/me/photos/*', + async (req, res, ctx) => { + if ( + !microsoftApi.tokenHasScope( + req.headers.get('authorization')!.replace(/^Bearer /, ''), + 'User.Read', + ) + ) { + return res(ctx.status(403)); + } + const imageBuffer = new Uint8Array([104, 111, 119, 100, 121]).buffer; + return res( + ctx.set('Content-Length', imageBuffer.byteLength.toString()), + ctx.set('Content-Type', 'image/jpeg'), + ctx.body(imageBuffer), + ); + }, + ), + ); + response = { + cookie: jest.fn(), + end: jest.fn(), + json: jest.fn(), + setHeader: jest.fn(), + status: jest.fn(), + } as unknown as jest.Mocked; + response.status.mockReturnValue(response); + }); + + describe('#start', () => { + const randomBytes = jest.spyOn( + crypto, + 'randomBytes', + ) as unknown as jest.MockedFunction<(size: number) => Buffer>; + + afterEach(() => { + randomBytes.mockRestore(); + }); + + it('redirects to authorize URL', async () => { + randomBytes.mockReturnValue(Buffer.from(nonce, 'base64')); + + await provider.start( + { + query: { + env: 'development', + scope: 'email openid profile User.Read', + }, + } as unknown as express.Request, + response, + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Location', + 'https://login.microsoftonline.com/tenantId/oauth2/v2.0/authorize' + + '?response_type=code' + + `&redirect_uri=${encodeURIComponent( + 'http://backstage.test/api/auth/microsoft/handler/frame', + )}` + + `&scope=${encodeURIComponent('email openid profile User.Read')}` + + `&state=${state}` + + '&client_id=clientId', + ); + }); + }); + + describe('#handle', () => { + it('returns provider info and profile with photo data', async () => { + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode( + 'email openid profile User.Read', + ), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.end).toHaveBeenCalledWith( + expect.stringContaining( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'email openid profile User.Read', + ), + scope: 'email openid profile User.Read', + expiresInSeconds: 123, + idToken: 'header.e30K.microsoft', + }, + profile: { + email: 'conrad@example.com', + picture: 'data:image/jpeg;base64,aG93ZHk=', + displayName: 'Conrad', + }, + backstageIdentity: { + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }, + }, + }), + ), + ), + ); + }); + + it('returns access token for non-microsoft graph scope', async () => { + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode('aks-audience/user.read'), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.end).toHaveBeenCalledWith( + expect.stringContaining( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'aks-audience/user.read', + ), + scope: 'aks-audience/user.read', + expiresInSeconds: 123, + }, + profile: {}, + }, + }), + ), + ), + ); + }); + + it('sets refresh token', async () => { + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode( + 'email offline_access openid profile User.Read', + ), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.cookie).toHaveBeenCalledWith( + 'microsoft-refresh-token', + microsoftApi.generateRefreshToken( + 'email offline_access openid profile User.Read', + ), + { + domain: 'backstage.test', + httpOnly: true, + maxAge: 86400000000, + path: '/api/auth/microsoft', + sameSite: 'lax', + secure: false, + }, + ); + }); + + it('omits photo data when fetching it fails', async () => { + server.use( + rest.get('https://graph.microsoft.com/v1.0/me/photos/*', (_, res) => + res.networkError('remote hung up'), + ), + ); + + await provider.frameHandler( + { + query: { + env: 'development', + code: microsoftApi.generateAuthCode( + 'email openid profile User.Read', + ), + state, + }, + cookies: { + 'microsoft-nonce': nonce, + }, + } as unknown as express.Request, + response, + ); + + expect(response.end).toHaveBeenCalledWith( + expect.stringContaining( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'email openid profile User.Read', + ), + scope: 'email openid profile User.Read', + expiresInSeconds: 123, + idToken: 'header.e30K.microsoft', + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + }, + backstageIdentity: { + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }, + }, + }), + ), + ), + ); + }); + }); + + describe('#refresh', () => { + it('returns provider info and profile with photo data', async () => { + await provider.refresh!( + { + query: { + env: 'development', + scope: 'email openid profile User.Read', + }, + header: jest.fn(_ => 'XMLHttpRequest'), + cookies: { + 'microsoft-refresh-token': microsoftApi.generateRefreshToken( + 'email openid profile User.Read', + ), + }, + get: jest.fn(), + } as unknown as express.Request, + response, + ); + + expect(response.json).toHaveBeenCalledWith( + expect.objectContaining({ + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'email openid profile User.Read', + ), + scope: 'email openid profile User.Read', + expiresInSeconds: 123, + idToken: 'header.e30K.microsoft', + }, + profile: { + email: 'conrad@example.com', + picture: 'data:image/jpeg;base64,aG93ZHk=', + displayName: 'Conrad', + }, + }), + ); + }); + + it('returns access token for non-microsoft graph scope', async () => { + await provider.refresh!( + { + query: { + env: 'development', + scope: 'aks-audience/user.read', + }, + header: jest.fn(_ => 'XMLHttpRequest'), + cookies: { + 'microsoft-refresh-token': microsoftApi.generateRefreshToken( + 'aks-audience/user.read', + ), + }, + get: jest.fn(), + } as unknown as express.Request, + response, + ); + + expect(response.json).toHaveBeenCalledWith({ + providerInfo: { + accessToken: microsoftApi.generateAccessToken( + 'aks-audience/user.read', + ), + expiresInSeconds: 123, + scope: 'aks-audience/user.read', + }, + profile: {}, + }); + }); + + it('returns backstage identity', async () => { + await provider.refresh!( + { + query: { + env: 'development', + scope: 'email openid profile User.Read', + }, + header: jest.fn(_ => 'XMLHttpRequest'), + cookies: { + 'microsoft-refresh-token': microsoftApi.generateRefreshToken( + 'email openid profile User.Read', + ), + }, + get: jest.fn(), + } as unknown as express.Request, + response, + ); + + expect(response.json).toHaveBeenCalledWith( + expect.objectContaining({ + backstageIdentity: expect.objectContaining({ + token: mockBackstageToken, + }), + }), + ); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 8947c7432b..8fc8459f10 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,25 +14,218 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; -import { OAuthResult } from '../../lib/oauth'; +import express from 'express'; +import passport from 'passport'; +import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; +import { + encodeState, + OAuthAdapter, + OAuthEnvironmentHandler, + OAuthHandlers, + OAuthProviderOptions, + OAuthRefreshRequest, + OAuthResponse, + OAuthResult, + OAuthStartRequest, +} from '../../lib/oauth'; +import { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, + PassportDoneCallback, +} from '../../lib/passport'; +import { + AuthHandler, + OAuthStartResponse, + SignInResolver, + AuthResolverContext, +} from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { - commonSignInResolvers, - createOAuthProviderFactory, -} from '@backstage/plugin-auth-node'; -import { - adaptLegacyOAuthHandler, - adaptLegacyOAuthSignInResolver, - adaptOAuthSignInResolverToLegacy, -} from '../../lib/legacy'; -import { - microsoftAuthenticator, - microsoftSignInResolvers, -} from '@backstage/plugin-auth-backend-module-microsoft-provider'; + commonByEmailLocalPartResolver, + commonByEmailResolver, +} from '../resolvers'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import fetch from 'node-fetch'; +import { decodeJwt } from 'jose'; +import { Profile as PassportProfile } from 'passport'; +import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session'; + +type PrivateInfo = { + refreshToken: string; +}; + +type Options = OAuthProviderOptions & { + signInResolver?: SignInResolver; + authHandler: AuthHandler; + logger: LoggerService; + resolverContext: AuthResolverContext; + authorizationUrl?: string; + tokenUrl?: string; +}; + +export class MicrosoftAuthProvider implements OAuthHandlers { + private readonly _strategy: MicrosoftStrategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly logger: LoggerService; + private readonly resolverContext: AuthResolverContext; + + constructor(options: Options) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.logger = options.logger; + this.resolverContext = options.resolverContext; + + this._strategy = new MicrosoftStrategy( + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + authorizationURL: options.authorizationUrl, + tokenURL: options.tokenUrl, + passReqToCallback: false, + skipUserProfile: ( + accessToken: string, + done: (err: unknown, skip: boolean) => void, + ) => { + done(null, this.skipUserProfile(accessToken)); + }, + }, + ( + accessToken: any, + refreshToken: any, + params: any, + fullProfile: passport.Profile, + done: PassportDoneCallback, + ) => { + done(undefined, { fullProfile, accessToken, params }, { refreshToken }); + }, + ); + } + + private skipUserProfile = (accessToken: string): boolean => { + const { aud, scp } = decodeJwt(accessToken); + const hasGraphReadScope = + aud === '00000003-0000-0000-c000-000000000000' && + (scp as string) + .split(' ') + .map(s => s.toLowerCase()) + .includes('user.read'); + return !hasGraphReadScope; + }; + + async start(req: OAuthStartRequest): Promise { + return await executeRedirectStrategy(req, this._strategy, { + scope: req.scope, + state: encodeState(req.state), + }); + } + + async handler(req: express.Request) { + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, + PrivateInfo + >(req, this._strategy); + + return { + response: await this.handleResult(result), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); + + return { + response: await this.handleResult({ + params, + accessToken, + ...(!this.skipUserProfile(accessToken) && { + fullProfile: await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + ), + }), + }), + refreshToken, + }; + } + + private async handleResult(result: { + fullProfile?: PassportProfile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; + }): Promise { + let profile = {}; + if (result.fullProfile) { + const photo = await this.getUserPhoto(result.accessToken); + result.fullProfile.photos = photo ? [{ value: photo }] : undefined; + ({ profile } = await this.authHandler( + result as OAuthResult, + this.resolverContext, + )); + } + + const expiresInSeconds = + result.params.expires_in === undefined + ? BACKSTAGE_SESSION_EXPIRATION + : Math.min(result.params.expires_in, BACKSTAGE_SESSION_EXPIRATION); + + return { + providerInfo: { + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds, + ...{ idToken: result.params.id_token }, + }, + profile, + ...(result.fullProfile && + this.signInResolver && { + backstageIdentity: await this.signInResolver( + { result: result as OAuthResult, profile }, + this.resolverContext, + ), + }), + }; + } + + private async getUserPhoto(accessToken: string): Promise { + try { + const res = await fetch( + 'https://graph.microsoft.com/v1.0/me/photos/48x48/$value', + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + const data = await res.buffer(); + + return `data:image/jpeg;base64,${data.toString('base64')}`; + } catch (error) { + this.logger.warn( + `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, + ); + return undefined; + } + } +} /** - * Auth provider integration for GitLab auth + * Auth provider integration for Microsoft auth * * @public */ @@ -48,21 +241,75 @@ export const microsoft = createAuthProviderIntegration({ * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ resolver: SignInResolver; }; }) { - return createOAuthProviderFactory({ - authenticator: microsoftAuthenticator, - profileTransform: adaptLegacyOAuthHandler(options?.authHandler), - signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), - }); + return ({ providerId, globalConfig, config, logger, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantId = envConfig.getString('tenantId'); + + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile ?? {}, params.id_token), + }); + + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + logger, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + providerId, + callbackUrl, + }); + }); + }, + resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, + /** + * Looks up the user by matching their email to the `microsoft.com/email` annotation. + */ + emailMatchingUserEntityAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Microsoft profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'microsoft.com/email': profile.email, + }, + }); + }; + }, }, - resolvers: adaptOAuthSignInResolverToLegacy({ - emailLocalPartMatchingUserEntityName: - commonSignInResolvers.emailLocalPartMatchingUserEntityName(), - emailMatchingUserEntityProfileEmail: - commonSignInResolvers.emailMatchingUserEntityProfileEmail(), - emailMatchingUserEntityAnnotation: - microsoftSignInResolvers.emailMatchingUserEntityAnnotation(), - }), }); diff --git a/yarn.lock b/yarn.lock index deb4f1a532..fddf4643ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4827,7 +4827,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-auth-backend-module-microsoft-provider@workspace:^, @backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider": +"@backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider" dependencies: @@ -4904,7 +4904,6 @@ __metadata: "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^" "@backstage/plugin-auth-backend-module-google-provider": "workspace:^" - "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" From 11aa8d4ee86b868150483b912502fb83fc4b40f4 Mon Sep 17 00:00:00 2001 From: Alex Eftimie Date: Mon, 23 Oct 2023 10:51:16 +0000 Subject: [PATCH 59/70] Update api-reports Signed-off-by: Alex Eftimie --- plugins/scaffolder-backend/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index ba7576b1bb..1f8615b477 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -659,6 +659,7 @@ export const createPublishGithubPullRequestAction: ( reviewers?: string[] | undefined; teamReviewers?: string[] | undefined; commitMessage?: string | undefined; + update?: boolean | undefined; }, JsonObject >; From c5aa242ce3ce66fde76abd7916ea863bbccaa507 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Oct 2023 14:28:27 +0200 Subject: [PATCH 60/70] Update plugins/code-coverage-backend/README.md Signed-off-by: Patrik Oldsberg --- plugins/code-coverage-backend/README.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 4d5c473c56..2b22b0ad1d 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -74,20 +74,7 @@ The code coverage backend plugin has support for the [new backend system](https: In your `packages/backend/src/index.ts` make the following changes: ```diff - import { createBackend } from '@backstage/backend-defaults'; -+ import { codeCoveragePlugin } from '@backstage/plugin-code-coverage-backend'; - const backend = createBackend(); - // ... other feature additions -+ backend.add(codeCoveragePlugin()); - backend.start(); -``` - -Alternatively, you can actually remove the import line above, and do this instead. - -```diff - backend.add(explorePlugin()); + backend.add(import('@backstage/plugin-explore-backend')); - ``` ## Configuring your entity From 2b91568fa3c4ddeee8e982621c06a57ee1cd5544 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:07:53 +0200 Subject: [PATCH 61/70] refactor(catalog): rename filters extensions file Signed-off-by: Camila Belo --- ...uiltInFilterExtensions.tsx => filters.tsx} | 2 +- plugins/catalog/src/alpha/plugin.tsx | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) rename plugins/catalog/src/alpha/{builtInFilterExtensions.tsx => filters.tsx} (98%) diff --git a/plugins/catalog/src/alpha/builtInFilterExtensions.tsx b/plugins/catalog/src/alpha/filters.tsx similarity index 98% rename from plugins/catalog/src/alpha/builtInFilterExtensions.tsx rename to plugins/catalog/src/alpha/filters.tsx index 5a7575c4f5..7b73c2d14b 100644 --- a/plugins/catalog/src/alpha/builtInFilterExtensions.tsx +++ b/plugins/catalog/src/alpha/filters.tsx @@ -109,7 +109,7 @@ const CatalogUserListFilter = createCatalogFilterExtension({ }, }); -export const builtInFilterExtensions = [ +export default [ CatalogEntityTagFilter, CatalogEntityKindFilter, CatalogEntityTypeFilter, diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index d9d2878213..9f6094d288 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -15,7 +15,9 @@ */ import React from 'react'; +import Grid from '@material-ui/core/Grid'; import HomeIcon from '@material-ui/icons/Home'; + import { createApiFactory, discoveryApiRef, @@ -23,7 +25,6 @@ import { storageApiRef, } from '@backstage/core-plugin-api'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -import { CatalogClient } from '@backstage/catalog-client'; import { createApiExtension, createPageExtension, @@ -32,6 +33,8 @@ import { coreExtensionData, createExtensionInput, } from '@backstage/frontend-plugin-api'; + +import { CatalogClient } from '@backstage/catalog-client'; import { AsyncEntityProvider, catalogApiRef, @@ -44,6 +47,7 @@ import { entityContentTitleExtensionDataRef, } from '@backstage/plugin-catalog-react/alpha'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; + import { DefaultStarredEntitiesApi } from '../apis'; import { createComponentRouteRef, @@ -51,9 +55,9 @@ import { rootRouteRef, viewTechDocRouteRef, } from '../routes'; -import { builtInFilterExtensions } from './builtInFilterExtensions'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; -import Grid from '@material-ui/core/Grid'; + +import filters from './filters'; /** @alpha */ export const CatalogApi = createApiExtension({ @@ -99,8 +103,11 @@ const CatalogIndexPage = createPageExtension({ }, loader: async ({ inputs }) => { const { BaseCatalogPage } = await import('../components/CatalogPage'); - const filters = inputs.filters.map(filter => filter.element); - return {filters}} />; + return ( + {inputs.filters.map(filter => filter.element)}} + /> + ); }, }); @@ -188,6 +195,7 @@ export default createPlugin({ createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef), }, extensions: [ + ...filters, CatalogApi, StarredEntitiesApi, CatalogSearchResultListItemExtension, @@ -196,6 +204,5 @@ export default createPlugin({ CatalogNavItem, OverviewEntityContent, EntityAboutCard, - ...builtInFilterExtensions, ], }); From 12e562f379e4fecf145aba298750d2d983e9d808 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:10:22 +0200 Subject: [PATCH 62/70] refactor(catalog): extract apis extensions file Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/apis.tsx | 51 ++++++++++++++++++++++++++++ plugins/catalog/src/alpha/plugin.tsx | 37 ++------------------ 2 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 plugins/catalog/src/alpha/apis.tsx diff --git a/plugins/catalog/src/alpha/apis.tsx b/plugins/catalog/src/alpha/apis.tsx new file mode 100644 index 0000000000..a887a33a04 --- /dev/null +++ b/plugins/catalog/src/alpha/apis.tsx @@ -0,0 +1,51 @@ +/* + * 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 { + createApiFactory, + discoveryApiRef, + fetchApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { CatalogClient } from '@backstage/catalog-client'; +import { createApiExtension } from '@backstage/frontend-plugin-api'; +import { + catalogApiRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { DefaultStarredEntitiesApi } from '../apis'; + +export const CatalogApi = createApiExtension({ + factory: createApiFactory({ + api: catalogApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new CatalogClient({ discoveryApi, fetchApi }), + }), +}); + +export const StarredEntitiesApi = createApiExtension({ + factory: createApiFactory({ + api: starredEntitiesApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + }), +}); + +export default [CatalogApi, StarredEntitiesApi]; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 9f6094d288..c40bb55aa5 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -18,15 +18,8 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; import HomeIcon from '@material-ui/icons/Home'; -import { - createApiFactory, - discoveryApiRef, - fetchApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; import { - createApiExtension, createPageExtension, createPlugin, createNavItemExtension, @@ -34,12 +27,9 @@ import { createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { CatalogClient } from '@backstage/catalog-client'; import { AsyncEntityProvider, - catalogApiRef, entityRouteRef, - starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { createEntityContentExtension, @@ -48,7 +38,6 @@ import { } from '@backstage/plugin-catalog-react/alpha'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; -import { DefaultStarredEntitiesApi } from '../apis'; import { createComponentRouteRef, createFromTemplateRouteRef, @@ -57,30 +46,9 @@ import { } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; +import apis from './apis'; import filters from './filters'; -/** @alpha */ -export const CatalogApi = createApiExtension({ - factory: createApiFactory({ - api: catalogApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new CatalogClient({ discoveryApi, fetchApi }), - }), -}); - -/** @alpha */ -export const StarredEntitiesApi = createApiExtension({ - factory: createApiFactory({ - api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), - }), -}); - /** @alpha */ export const CatalogSearchResultListItemExtension = createSearchResultListItemExtension({ @@ -195,9 +163,8 @@ export default createPlugin({ createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef), }, extensions: [ + ...apis, ...filters, - CatalogApi, - StarredEntitiesApi, CatalogSearchResultListItemExtension, CatalogIndexPage, CatalogEntityPage, From 3f65bb7208b93cbbed295be3d8098989947e9ef9 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:12:00 +0200 Subject: [PATCH 63/70] refactor(catalog): extract pages extensions file Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/pages.tsx | 83 ++++++++++++++++++++++++++++ plugins/catalog/src/alpha/plugin.tsx | 66 +--------------------- 2 files changed, 86 insertions(+), 63 deletions(-) create mode 100644 plugins/catalog/src/alpha/pages.tsx diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx new file mode 100644 index 0000000000..6270ae777d --- /dev/null +++ b/plugins/catalog/src/alpha/pages.tsx @@ -0,0 +1,83 @@ +/* + * 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 from 'react'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +import { + createPageExtension, + coreExtensionData, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { + AsyncEntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { entityContentTitleExtensionDataRef } from '@backstage/plugin-catalog-react/alpha'; +import { rootRouteRef } from '../routes'; +import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; + +export const CatalogIndexPage = createPageExtension({ + id: 'plugin.catalog.page.index', + defaultPath: '/catalog', + routeRef: convertLegacyRouteRef(rootRouteRef), + inputs: { + filters: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => { + const { BaseCatalogPage } = await import('../components/CatalogPage'); + const filters = inputs.filters.map(filter => filter.element); + return {filters}} />; + }, +}); + +export const CatalogEntityPage = createPageExtension({ + id: 'plugin.catalog.page.entity', + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + inputs: { + contents: createExtensionInput({ + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: entityContentTitleExtensionDataRef, + }), + }, + loader: async ({ inputs }) => { + const { EntityLayout } = await import('../components/EntityLayout'); + const Component = () => { + return ( + + + {inputs.contents.map(content => ( + + {content.element} + + ))} + + + ); + }; + return ; + }, +}); + +export default [CatalogIndexPage, CatalogEntityPage]; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index c40bb55aa5..67d1f036ee 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -20,21 +20,16 @@ import HomeIcon from '@material-ui/icons/Home'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; import { - createPageExtension, createPlugin, createNavItemExtension, coreExtensionData, createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { - AsyncEntityProvider, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { createEntityContentExtension, createEntityCardExtension, - entityContentTitleExtensionDataRef, } from '@backstage/plugin-catalog-react/alpha'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; @@ -44,9 +39,9 @@ import { rootRouteRef, viewTechDocRouteRef, } from '../routes'; -import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; import apis from './apis'; +import pages from './pages'; import filters from './filters'; /** @alpha */ @@ -60,60 +55,6 @@ export const CatalogSearchResultListItemExtension = ), }); -const CatalogIndexPage = createPageExtension({ - id: 'plugin.catalog.page.index', - defaultPath: '/catalog', - routeRef: convertLegacyRouteRef(rootRouteRef), - inputs: { - filters: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - loader: async ({ inputs }) => { - const { BaseCatalogPage } = await import('../components/CatalogPage'); - return ( - {inputs.filters.map(filter => filter.element)}} - /> - ); - }, -}); - -const CatalogEntityPage = createPageExtension({ - id: 'plugin.catalog.page.entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - inputs: { - contents: createExtensionInput({ - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - title: entityContentTitleExtensionDataRef, - }), - }, - loader: async ({ inputs }) => { - const { EntityLayout } = await import('../components/EntityLayout'); - const Component = () => { - return ( - - - {inputs.contents.map(content => ( - - {content.element} - - ))} - - - ); - }; - return ; - }, -}); - const EntityAboutCard = createEntityCardExtension({ id: 'about', loader: async () => @@ -164,10 +105,9 @@ export default createPlugin({ }, extensions: [ ...apis, + ...pages, ...filters, CatalogSearchResultListItemExtension, - CatalogIndexPage, - CatalogEntityPage, CatalogNavItem, OverviewEntityContent, EntityAboutCard, From 870e4186eff059dabcb23117adc301f197997faa Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:13:04 +0200 Subject: [PATCH 64/70] refactor(catalog): extract entity cards extensions file Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/entityCards.tsx | 173 ++++++++++++++++++++++ plugins/catalog/src/alpha/plugin.tsx | 16 +- 2 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 plugins/catalog/src/alpha/entityCards.tsx diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx new file mode 100644 index 0000000000..9faf0b9517 --- /dev/null +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -0,0 +1,173 @@ +/* + * 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 from 'react'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; +import { createSchemaFromZod } from '@backstage/frontend-plugin-api'; + +export const EntityAboutCard = createEntityCardExtension({ + id: 'about', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + }), + ), + loader: async ({ config }) => + import('../components/AboutCard').then(m => ( + + )), +}); + +export const EntityLinksCard = createEntityCardExtension({ + id: 'links', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).optional(), + cols: z + .union([ + z.number(), + z.object({ + xs: z.number(), + sm: z.number(), + md: z.number(), + lg: z.number(), + xl: z.number(), + }), + ]) + .optional(), + }), + ), + loader: async ({ config }) => + import('../components/EntityLinksCard').then(m => { + return ; + }), +}); + +export const EntityLabelsCard = createEntityCardExtension({ + id: 'labels', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).optional(), + title: z.string().optional(), + }), + ), + loader: async ({ config }) => + import('../components/EntityLabelsCard').then(m => ( + + )), +}); + +export const EntityDependsOnComponentsCard = createEntityCardExtension({ + id: 'dependsOn.components', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + title: z.string().optional(), + // TODO: columns, tableOptions + }), + ), + loader: async ({ config }) => + import('../components/DependsOnComponentsCard').then(m => ( + + )), +}); + +export const EntityDependsOnResourcesCard = createEntityCardExtension({ + id: 'dependsOn.resources', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + title: z.string().optional(), + // TODO: columns, tableOptions + }), + ), + loader: async ({ config }) => + import('../components/DependsOnResourcesCard').then(m => ( + + )), +}); + +export const EntityHasComponentsCard = createEntityCardExtension({ + id: 'has.components', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + title: z.string().optional(), + }), + ), + loader: async ({ config }) => + import('../components/HasComponentsCard').then(m => ( + + )), +}); + +export const EntityHasResourcesCard = createEntityCardExtension({ + id: 'has.resources', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + title: z.string().optional(), + }), + ), + loader: async ({ config }) => + import('../components/HasResourcesCard').then(m => ( + + )), +}); + +export const EntityHasSubcomponentsCard = createEntityCardExtension({ + id: 'has.subcomponents', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + title: z.string().optional(), + // TODO: tableOptions + }), + ), + loader: async ({ config }) => + import('../components/HasSubcomponentsCard').then(m => ( + + )), +}); + +export const EntityHasSystemsCard = createEntityCardExtension({ + id: 'has.systems', + configSchema: createSchemaFromZod(z => + z.object({ + variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), + title: z.string().optional(), + }), + ), + loader: async ({ config }) => + import('../components/HasSystemsCard').then(m => ( + + )), +}); + +export default [ + EntityAboutCard, + EntityLinksCard, + EntityLabelsCard, + EntityDependsOnComponentsCard, + EntityDependsOnResourcesCard, + EntityHasComponentsCard, + EntityHasResourcesCard, + EntityHasSubcomponentsCard, + EntityHasSystemsCard, +]; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 67d1f036ee..761a00c29b 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -27,10 +27,7 @@ import { } from '@backstage/frontend-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; -import { - createEntityContentExtension, - createEntityCardExtension, -} from '@backstage/plugin-catalog-react/alpha'; +import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; import { @@ -43,6 +40,7 @@ import { import apis from './apis'; import pages from './pages'; import filters from './filters'; +import entityCards from './entityCards'; /** @alpha */ export const CatalogSearchResultListItemExtension = @@ -55,14 +53,6 @@ export const CatalogSearchResultListItemExtension = ), }); -const EntityAboutCard = createEntityCardExtension({ - id: 'about', - loader: async () => - import('../components/AboutCard').then(m => ( - - )), -}); - const OverviewEntityContent = createEntityContentExtension({ id: 'overview', defaultPath: '/', @@ -107,9 +97,9 @@ export default createPlugin({ ...apis, ...pages, ...filters, + ...entityCards, CatalogSearchResultListItemExtension, CatalogNavItem, OverviewEntityContent, - EntityAboutCard, ], }); From 81497c8a4125cf779d3f12d53c8d1f504d413d7d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:17:02 +0200 Subject: [PATCH 65/70] refactor(catalog): extract nav items extensions file Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/navItems.tsx | 29 ++++++++++++++++++++++++++ plugins/catalog/src/alpha/plugin.tsx | 12 ++--------- 2 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 plugins/catalog/src/alpha/navItems.tsx diff --git a/plugins/catalog/src/alpha/navItems.tsx b/plugins/catalog/src/alpha/navItems.tsx new file mode 100644 index 0000000000..b6481fd764 --- /dev/null +++ b/plugins/catalog/src/alpha/navItems.tsx @@ -0,0 +1,29 @@ +/* + * 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 HomeIcon from '@material-ui/icons/Home'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +import { createNavItemExtension } from '@backstage/frontend-plugin-api'; +import { rootRouteRef } from '../routes'; + +export const CatalogIndexNavItem = createNavItemExtension({ + id: 'catalog.nav.index', + routeRef: convertLegacyRouteRef(rootRouteRef), + title: 'Catalog', + icon: HomeIcon, +}); + +export default [CatalogIndexNavItem]; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 761a00c29b..10e516874f 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -16,12 +16,10 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; -import HomeIcon from '@material-ui/icons/Home'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; import { createPlugin, - createNavItemExtension, coreExtensionData, createExtensionInput, } from '@backstage/frontend-plugin-api'; @@ -40,6 +38,7 @@ import { import apis from './apis'; import pages from './pages'; import filters from './filters'; +import navItems from './navItems'; import entityCards from './entityCards'; /** @alpha */ @@ -74,13 +73,6 @@ const OverviewEntityContent = createEntityContentExtension({ ), }); -const CatalogNavItem = createNavItemExtension({ - id: 'catalog.nav.index', - routeRef: convertLegacyRouteRef(rootRouteRef), - title: 'Catalog', - icon: HomeIcon, -}); - /** @alpha */ export default createPlugin({ id: 'catalog', @@ -97,9 +89,9 @@ export default createPlugin({ ...apis, ...pages, ...filters, + ...navItems, ...entityCards, CatalogSearchResultListItemExtension, - CatalogNavItem, OverviewEntityContent, ], }); From 29ccace57b72f2a44c576291456ae518b16bf67b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:19:47 +0200 Subject: [PATCH 66/70] refactor(catalog): extract search result items extensions file Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/plugin.tsx | 15 ++-------- .../catalog/src/alpha/searchResultItems.tsx | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 plugins/catalog/src/alpha/searchResultItems.tsx diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 10e516874f..2c85e2bc51 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -26,7 +26,6 @@ import { import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; import { createComponentRouteRef, @@ -40,17 +39,7 @@ import pages from './pages'; import filters from './filters'; import navItems from './navItems'; import entityCards from './entityCards'; - -/** @alpha */ -export const CatalogSearchResultListItemExtension = - createSearchResultListItemExtension({ - id: 'catalog', - predicate: result => result.type === 'software-catalog', - component: () => - import('../components/CatalogSearchResultListItem').then( - m => m.CatalogSearchResultListItem, - ), - }); +import searchResultItems from './searchResultItems'; const OverviewEntityContent = createEntityContentExtension({ id: 'overview', @@ -91,7 +80,7 @@ export default createPlugin({ ...filters, ...navItems, ...entityCards, - CatalogSearchResultListItemExtension, + ...searchResultItems, OverviewEntityContent, ], }); diff --git a/plugins/catalog/src/alpha/searchResultItems.tsx b/plugins/catalog/src/alpha/searchResultItems.tsx new file mode 100644 index 0000000000..f677869136 --- /dev/null +++ b/plugins/catalog/src/alpha/searchResultItems.tsx @@ -0,0 +1,29 @@ +/* + * 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 { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; + +export const CatalogSearchResultListItemExtension = + createSearchResultListItemExtension({ + id: 'catalog', + predicate: result => result.type === 'software-catalog', + component: () => + import('../components/CatalogSearchResultListItem').then( + m => m.CatalogSearchResultListItem, + ), + }); + +export default [CatalogSearchResultListItemExtension]; From 24dced7352c44eb3f093bd7bd64a89206660226d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 17:37:22 +0200 Subject: [PATCH 67/70] refactor(catalog): extract entity contentes extensions file Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/entityContents.tsx | 46 ++++++++++++++++++++ plugins/catalog/src/alpha/plugin.tsx | 34 ++------------- 2 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 plugins/catalog/src/alpha/entityContents.tsx diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx new file mode 100644 index 0000000000..0e26373b5e --- /dev/null +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -0,0 +1,46 @@ +/* + * 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 from 'react'; +import { Grid } from '@material-ui/core'; +import { + coreExtensionData, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; + +export const OverviewEntityContent = createEntityContentExtension({ + id: 'overview', + defaultPath: '/', + defaultTitle: 'Overview', + disabled: false, + inputs: { + cards: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => ( + + {inputs.cards.map(card => ( + + {card.element} + + ))} + + ), +}); + +export default [OverviewEntityContent]; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 2c85e2bc51..2f019ca33e 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -14,18 +14,10 @@ * limitations under the License. */ -import React from 'react'; -import Grid from '@material-ui/core/Grid'; - import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -import { - createPlugin, - coreExtensionData, - createExtensionInput, -} from '@backstage/frontend-plugin-api'; +import { createPlugin } from '@backstage/frontend-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; -import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; import { createComponentRouteRef, @@ -39,29 +31,9 @@ import pages from './pages'; import filters from './filters'; import navItems from './navItems'; import entityCards from './entityCards'; +import entityContents from './entityContents'; import searchResultItems from './searchResultItems'; -const OverviewEntityContent = createEntityContentExtension({ - id: 'overview', - defaultPath: '/', - defaultTitle: 'Overview', - disabled: false, - inputs: { - cards: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - loader: async ({ inputs }) => ( - - {inputs.cards.map(card => ( - - {card.element} - - ))} - - ), -}); - /** @alpha */ export default createPlugin({ id: 'catalog', @@ -80,7 +52,7 @@ export default createPlugin({ ...filters, ...navItems, ...entityCards, + ...entityContents, ...searchResultItems, - OverviewEntityContent, ], }); From 8a8445663b49d22ebb8ad52b871400d192818b27 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 21 Oct 2023 13:33:31 +0200 Subject: [PATCH 68/70] chore: add changeset file Signed-off-by: Camila Belo --- .changeset/fast-bears-lick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fast-bears-lick.md diff --git a/.changeset/fast-bears-lick.md b/.changeset/fast-bears-lick.md new file mode 100644 index 0000000000..8e0dc8e60b --- /dev/null +++ b/.changeset/fast-bears-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Migrate catalog entity cards to new frontend system extension format. From 150f2a8797eb9414d13b809d269690ab58a86f7c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 23 Oct 2023 14:28:24 +0200 Subject: [PATCH 69/70] refactor(catalog): remove cards config for now Signed-off-by: Camila Belo --- plugins/catalog/src/alpha/entityCards.tsx | 107 ++++------------------ 1 file changed, 18 insertions(+), 89 deletions(-) diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 9faf0b9517..cc30928fcf 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -16,147 +16,76 @@ import React from 'react'; import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; -import { createSchemaFromZod } from '@backstage/frontend-plugin-api'; export const EntityAboutCard = createEntityCardExtension({ id: 'about', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/AboutCard').then(m => ( - + )), }); export const EntityLinksCard = createEntityCardExtension({ id: 'links', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).optional(), - cols: z - .union([ - z.number(), - z.object({ - xs: z.number(), - sm: z.number(), - md: z.number(), - lg: z.number(), - xl: z.number(), - }), - ]) - .optional(), - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/EntityLinksCard').then(m => { - return ; + return ; }), }); export const EntityLabelsCard = createEntityCardExtension({ id: 'labels', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).optional(), - title: z.string().optional(), - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/EntityLabelsCard').then(m => ( - + )), }); export const EntityDependsOnComponentsCard = createEntityCardExtension({ id: 'dependsOn.components', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - title: z.string().optional(), - // TODO: columns, tableOptions - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/DependsOnComponentsCard').then(m => ( - + )), }); export const EntityDependsOnResourcesCard = createEntityCardExtension({ id: 'dependsOn.resources', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - title: z.string().optional(), - // TODO: columns, tableOptions - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/DependsOnResourcesCard').then(m => ( - + )), }); export const EntityHasComponentsCard = createEntityCardExtension({ id: 'has.components', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - title: z.string().optional(), - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/HasComponentsCard').then(m => ( - + )), }); export const EntityHasResourcesCard = createEntityCardExtension({ id: 'has.resources', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - title: z.string().optional(), - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/HasResourcesCard').then(m => ( - + )), }); export const EntityHasSubcomponentsCard = createEntityCardExtension({ id: 'has.subcomponents', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - title: z.string().optional(), - // TODO: tableOptions - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/HasSubcomponentsCard').then(m => ( - + )), }); export const EntityHasSystemsCard = createEntityCardExtension({ id: 'has.systems', - configSchema: createSchemaFromZod(z => - z.object({ - variant: z.enum(['gridItem', 'flex', 'fullHeight']).default('gridItem'), - title: z.string().optional(), - }), - ), - loader: async ({ config }) => + loader: async () => import('../components/HasSystemsCard').then(m => ( - + )), }); From 5b46f06a4f1956a55e190308198045f6797febda Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 23 Oct 2023 16:32:55 +0200 Subject: [PATCH 70/70] chore: fix needless re-rendering Signed-off-by: blam --- .../src/next/components/Form/Form.tsx | 45 +++++++++++-------- .../src/next/components/Stepper/Stepper.tsx | 7 ++- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Form/Form.tsx b/plugins/scaffolder-react/src/next/components/Form/Form.tsx index 85e75e2c0d..8717c8b55a 100644 --- a/plugins/scaffolder-react/src/next/components/Form/Form.tsx +++ b/plugins/scaffolder-react/src/next/components/Form/Form.tsx @@ -32,27 +32,34 @@ const WrappedForm = withTheme(MuiTheme); export const Form = (props: PropsWithChildren) => { // This is where we unbreak the changes from RJSF, and make it work with our custom fields so we don't pass on this // breaking change to our users. We will look more into a better API for this in scaffolderv2. - const wrappedFields = Object.fromEntries( - Object.entries(props.fields ?? {}).map(([key, Component]) => [ - key, - (wrapperProps: FieldProps) => { - return ( - - ); - }, - ]), + const wrappedFields = React.useMemo( + () => + Object.fromEntries( + Object.entries(props.fields ?? {}).map(([key, Component]) => [ + key, + (wrapperProps: FieldProps) => { + return ( + + ); + }, + ]), + ), + [props.fields], ); - const templates = { - FieldTemplate, - DescriptionFieldTemplate, - ...props.templates, - }; + const templates = React.useMemo( + () => ({ + FieldTemplate, + DescriptionFieldTemplate, + ...props.templates, + }), + [props.templates], + ); return ( diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 1e43eba770..23e97c67e4 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -115,6 +115,11 @@ export const Stepper = (stepperProps: StepperProps) => { ); }, [props.extensions]); + const fields = useMemo( + () => ({ ...FieldOverrides, ...extensions }), + [extensions], + ); + const validators = useMemo(() => { return Object.fromEntries( props.extensions.map(({ name, validation }) => [name, validation]), @@ -197,7 +202,7 @@ export const Stepper = (stepperProps: StepperProps) => { schema={currentStep.schema} uiSchema={currentStep.uiSchema} onSubmit={handleNext} - fields={{ ...FieldOverrides, ...extensions }} + fields={fields} showErrorList={false} onChange={handleChange} {...(props.formProps ?? {})}