From b8970b89419f47bb44eed1012fd85d176faa5f64 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 8 Jun 2022 16:59:46 +0200 Subject: [PATCH 01/54] cli/create-github-app: Add members to permission selection list Signed-off-by: Johan Haals --- .changeset/green-parents-pay.md | 5 +++ .../GithubCreateAppServer.ts | 42 +++++++++---------- .../src/commands/create-github-app/index.ts | 39 ++++++++++++----- 3 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 .changeset/green-parents-pay.md diff --git a/.changeset/green-parents-pay.md b/.changeset/green-parents-pay.md new file mode 100644 index 0000000000..15ceab937d --- /dev/null +++ b/.changeset/green-parents-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Improved the `create-github-app` permissions selection prompt by converting it into a multi-select with clearer descriptions. The `members` permission is now also included in the list which is required for ingesting user data into the catalog. diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts index 0fe321541e..70b8c0ca53 100644 --- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts +++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts @@ -19,18 +19,6 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { request } from '@octokit/request'; import express, { Express, Request, Response } from 'express'; -const MANIFEST_DATA = { - default_events: ['create', 'delete', 'push', 'repository'], - default_permissions: { - contents: 'read', - metadata: 'read', - }, - name: 'Backstage-', - url: 'https://backstage.io', - description: 'GitHub App for Backstage', - public: false, -}; - const FORM_PAGE = ` @@ -62,17 +50,17 @@ export class GithubCreateAppServer { static async run(options: { org: string; - readWrite: boolean; + permissions: string[]; }): Promise { const encodedOrg = encodeURIComponent(options.org); const actionUrl = `https://github.com/organizations/${encodedOrg}/settings/apps/new`; - const server = new GithubCreateAppServer(actionUrl, options.readWrite); + const server = new GithubCreateAppServer(actionUrl, options.permissions); return server.start(); } private constructor( private readonly actionUrl: string, - private readonly readWrite: boolean, + private readonly permissions: string[], ) { const webhookId = crypto .randomBytes(15) @@ -121,21 +109,29 @@ export class GithubCreateAppServer { if (!baseUrl) { throw new Error('baseUrl is not set'); } + const manifest = { - ...MANIFEST_DATA, + default_events: ['create', 'delete', 'push', 'repository'], + default_permissions: { + metadata: 'read', + ...(this.permissions.includes('members') && { members: 'read' }), + ...(this.permissions.includes('read') && { contents: 'read' }), + ...(this.permissions.includes('write') && { + contents: 'write', + actions: 'write', + }), + }, + name: 'Backstage-', + url: 'https://backstage.io', + description: 'GitHub App for Backstage', + public: false, redirect_url: `${baseUrl}/callback`, hook_attributes: { url: this.webhookUrl, active: false, }, - ...(this.readWrite && { - default_permissions: { - contents: 'write', - actions: 'write', - metadata: 'read', - }, - }), }; + const manifestJson = JSON.stringify(manifest).replace(/\"/g, '"'); let body = FORM_PAGE; diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index 175530402e..6b3e732f18 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -26,19 +26,38 @@ import openBrowser from 'react-dev-utils/openBrowser'; // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest export default async (org: string) => { - const answers: Answers = await inquirer.prompt([ - { - type: 'list', - name: 'appType', - message: chalk.blue('What will the app be used for [required]'), - choices: ['Read and Write (needed by Software Templates)', 'Read only'], - }, - ]); - const readWrite = answers.appType !== 'Read only'; + const answers: Answers = await inquirer.prompt({ + name: 'appType', + type: 'checkbox', + message: + 'Select permissions [required] (these can be changed later but then require approvals in all installations)', + choices: [ + { + name: 'Read access to content (required by Software Catalog to ingest data from repositories)', + value: 'read', + checked: true, + }, + { + name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', + value: 'members', + checked: true, + }, + { + name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', + value: 'write', + }, + ], + }); + + if (answers.appType.length === 0) { + console.log(chalk.red('You must select at least one permission')); + process.exit(1); + } + await verifyGithubOrg(org); const { slug, name, ...config } = await GithubCreateAppServer.run({ org, - readWrite, + permissions: answers.appType, }); const fileName = `github-app-${slug}-credentials.yaml`; From 562bd4cfe4f37c3e0727d3346c6e323a9e9897d7 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 01:03:46 +0200 Subject: [PATCH 02/54] chore(gitlab MR action): remove projectId from template to because it is redundant Signed-off-by: djamaile --- .../builtin/publish/gitlabMergeRequest.ts | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 7259336b23..ab11787f05 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -34,7 +34,6 @@ export const createPublishGitlabMergeRequestAction = (options: { const { integrations } = options; return createTemplateAction<{ - projectid: string; repoUrl: string; title: string; description: string; @@ -45,7 +44,7 @@ export const createPublishGitlabMergeRequestAction = (options: { id: 'publish:gitlab:merge-request', schema: { input: { - required: ['projectid', 'repoUrl', 'targetPath', 'branchName'], + required: ['repoUrl', 'targetPath', 'branchName'], type: 'object', properties: { repoUrl: { @@ -53,11 +52,6 @@ export const createPublishGitlabMergeRequestAction = (options: { title: 'Repository Location', description: `Accepts the format 'gitlab.com/group_name/project_name' where 'project_name' is the repository name and 'group_name' is a group or username`, }, - projectid: { - type: 'string', - title: 'projectid', - description: 'Project ID/Name(slug) of the Gitlab Project', - }, title: { type: 'string', title: 'Merge Request Name', @@ -88,8 +82,8 @@ export const createPublishGitlabMergeRequestAction = (options: { output: { type: 'object', properties: { - projectid: { - title: 'Gitlab Project id/Name(slug)', + projectPath: { + title: 'Gitlab Project path', type: 'string', }, mergeRequestURL: { @@ -102,7 +96,9 @@ export const createPublishGitlabMergeRequestAction = (options: { }, async handler(ctx) { const repoUrl = ctx.input.repoUrl; - const { host } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const projectPath = `${owner}/${repo}`; + const integrationConfig = integrations.gitlab.byHost(host); const destinationBranch = ctx.input.branchName; @@ -140,14 +136,13 @@ export const createPublishGitlabMergeRequestAction = (options: { content: file.content.toString('base64'), execute_filemode: file.executable, })); - - const projects = await api.Projects.show(ctx.input.projectid); + const projects = await api.Projects.show(projectPath); const { default_branch: defaultBranch } = projects; try { await api.Branches.create( - ctx.input.projectid, + projectPath, destinationBranch, String(defaultBranch), ); @@ -157,7 +152,7 @@ export const createPublishGitlabMergeRequestAction = (options: { try { await api.Commits.create( - ctx.input.projectid, + projectPath, destinationBranch, ctx.input.title, actions, @@ -170,7 +165,7 @@ export const createPublishGitlabMergeRequestAction = (options: { try { const mergeRequestUrl = await api.MergeRequests.create( - ctx.input.projectid, + projectPath, destinationBranch, String(defaultBranch), ctx.input.title, @@ -178,7 +173,7 @@ export const createPublishGitlabMergeRequestAction = (options: { ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); - ctx.output('projectid', ctx.input.projectid); + ctx.output('project path: ', projectPath); ctx.output('mergeRequestUrl', mergeRequestUrl); } catch (e) { throw new InputError(`Merge request creation failed${e}`); From 35a26131b3a07f746c8173317ed5c31fd6920e32 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 01:17:05 +0200 Subject: [PATCH 03/54] chore: add changeset Signed-off-by: djamaile --- .changeset/forty-timers-cheer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/forty-timers-cheer.md diff --git a/.changeset/forty-timers-cheer.md b/.changeset/forty-timers-cheer.md new file mode 100644 index 0000000000..29815fba67 --- /dev/null +++ b/.changeset/forty-timers-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +refactored the gitlab publish merge request action to not use the `projectId` property. Instead use the `host` and `owner` property from the `RepoUrlPicker`. This decreases the amount of fields the user has to fill in. From 7f78d71537f8fb22fbee525ae9b4800c1f1c351f Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 01:31:28 +0200 Subject: [PATCH 04/54] chore: run api report command to prevent build from breaking Signed-off-by: djamaile --- plugins/scaffolder-backend/api-report.md | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 308696d59b..51588054c0 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -338,7 +338,6 @@ export function createPublishGitlabAction(options: { export const createPublishGitlabMergeRequestAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction<{ - projectid: string; repoUrl: string; title: string; description: string; From 34b84b4072fb3546e97319fc012d52557b4d3755 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 11:50:48 +0200 Subject: [PATCH 05/54] chore: make the changes backwards compatible to ease the migration Signed-off-by: djamaile --- .changeset/forty-timers-cheer.md | 5 +++-- .../actions/builtin/publish/gitlabMergeRequest.ts | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.changeset/forty-timers-cheer.md b/.changeset/forty-timers-cheer.md index 29815fba67..20ea9b90d4 100644 --- a/.changeset/forty-timers-cheer.md +++ b/.changeset/forty-timers-cheer.md @@ -1,5 +1,6 @@ --- -'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-backend': major --- -refactored the gitlab publish merge request action to not use the `projectId` property. Instead use the `host` and `owner` property from the `RepoUrlPicker`. This decreases the amount of fields the user has to fill in. +**DEPRECATION**: The `projectid` input parameters to the `publish:gitlab:merge-request`, it's no longer required as it can be decoded from the `repoUrl` input parameter. +**DEPRECATION**: The `projectid` output of the action in favour of `projectPath` diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index ab11787f05..d163a52d26 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -40,6 +40,8 @@ export const createPublishGitlabMergeRequestAction = (options: { branchName: string; targetPath: string; token?: string; + /** @deprecated */ + projectid?: string; }>({ id: 'publish:gitlab:merge-request', schema: { @@ -52,6 +54,12 @@ export const createPublishGitlabMergeRequestAction = (options: { title: 'Repository Location', description: `Accepts the format 'gitlab.com/group_name/project_name' where 'project_name' is the repository name and 'group_name' is a group or username`, }, + /** @deprecated */ + projectid: { + type: 'string', + title: 'projectid', + description: 'Project ID/Name(slug) of the Gitlab Project', + }, title: { type: 'string', title: 'Merge Request Name', @@ -82,6 +90,10 @@ export const createPublishGitlabMergeRequestAction = (options: { output: { type: 'object', properties: { + projectid: { + title: 'Gitlab Project id/Name(slug)', + type: 'string', + }, projectPath: { title: 'Gitlab Project path', type: 'string', @@ -173,7 +185,8 @@ export const createPublishGitlabMergeRequestAction = (options: { ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); - ctx.output('project path: ', projectPath); + ctx.output('projectid', projectPath); + ctx.output('projectPath', projectPath); ctx.output('mergeRequestUrl', mergeRequestUrl); } catch (e) { throw new InputError(`Merge request creation failed${e}`); From c8af5ea7b3b64fec05fdf4ce19ad517278326351 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 12:09:39 +0200 Subject: [PATCH 06/54] chore: run api report command to prevent build from breaking Signed-off-by: djamaile --- plugins/scaffolder-backend/api-report.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 51588054c0..fbb071cd51 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -344,6 +344,7 @@ export const createPublishGitlabMergeRequestAction: (options: { branchName: string; targetPath: string; token?: string | undefined; + projectid?: string | undefined; }>; // @public @@ -709,4 +710,8 @@ export class TemplateActionRegistry { // @public (undocumented) export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; + +// Warnings were encountered during analysis: +// +// src/scaffolder/actions/builtin/publish/gitlabMergeRequest.d.ts:16:9 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` From d507278a0c31422b7e33f99577a45690b8751fc0 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 12:40:21 +0200 Subject: [PATCH 07/54] chore: change major to minor because we haven't make any breaking changes Signed-off-by: djamaile --- .changeset/forty-timers-cheer.md | 2 +- .../scaffolder/actions/builtin/publish/gitlabMergeRequest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/forty-timers-cheer.md b/.changeset/forty-timers-cheer.md index 20ea9b90d4..9fba60e6ed 100644 --- a/.changeset/forty-timers-cheer.md +++ b/.changeset/forty-timers-cheer.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend': major +'@backstage/plugin-scaffolder-backend': minor --- **DEPRECATION**: The `projectid` input parameters to the `publish:gitlab:merge-request`, it's no longer required as it can be decoded from the `repoUrl` input parameter. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index d163a52d26..07c094cba9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -185,6 +185,7 @@ export const createPublishGitlabMergeRequestAction = (options: { ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); + /** @deprecated */ ctx.output('projectid', projectPath); ctx.output('projectPath', projectPath); ctx.output('mergeRequestUrl', mergeRequestUrl); From 890dfe614af5d28af2119ac23264dc7372a0a209 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 14:46:01 +0200 Subject: [PATCH 08/54] chore: give alternative to deprecated projectId Signed-off-by: djamaile --- plugins/scaffolder-backend/api-report.md | 4 ---- .../actions/builtin/publish/gitlabMergeRequest.ts | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index fbb071cd51..707b8e62ba 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -710,8 +710,4 @@ export class TemplateActionRegistry { // @public (undocumented) export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; - -// Warnings were encountered during analysis: -// -// src/scaffolder/actions/builtin/publish/gitlabMergeRequest.d.ts:16:9 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 07c094cba9..c5dd41f807 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -40,7 +40,7 @@ export const createPublishGitlabMergeRequestAction = (options: { branchName: string; targetPath: string; token?: string; - /** @deprecated */ + /** @deprecated Use projectPath instead */ projectid?: string; }>({ id: 'publish:gitlab:merge-request', @@ -54,7 +54,7 @@ export const createPublishGitlabMergeRequestAction = (options: { title: 'Repository Location', description: `Accepts the format 'gitlab.com/group_name/project_name' where 'project_name' is the repository name and 'group_name' is a group or username`, }, - /** @deprecated */ + /** @deprecated Use projectPath instead */ projectid: { type: 'string', title: 'projectid', @@ -185,7 +185,7 @@ export const createPublishGitlabMergeRequestAction = (options: { ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); - /** @deprecated */ + /** @deprecated Use projectPath instead */ ctx.output('projectid', projectPath); ctx.output('projectPath', projectPath); ctx.output('mergeRequestUrl', mergeRequestUrl); From eea8126171135c356f7e6bf30f0434e9634a7f57 Mon Sep 17 00:00:00 2001 From: ivgo Date: Tue, 7 Jun 2022 08:24:40 +0200 Subject: [PATCH 09/54] Gitlab entity provider Signed-off-by: ivgo --- .changeset/lazy-zoos-move.md | 5 + .changeset/perfect-mangos-allow.md | 5 + docs/integrations/gitlab/discovery.md | 59 ++++- .../catalog-backend-module-gitlab/config.d.ts | 57 +++++ .../package.json | 11 +- .../src/index.ts | 1 + .../src/lib/client.test.ts | 45 ++++ .../src/lib/client.ts | 8 + .../src/lib/index.ts | 7 +- .../src/lib/types.ts | 15 ++ .../GitlabDiscoveryEntityProvider.test.ts | 230 ++++++++++++++++++ .../GitlabDiscoveryEntityProvider.ts | 190 +++++++++++++++ .../src/providers/config.test.ts | 106 ++++++++ .../src/providers/config.ts | 50 ++++ .../src/providers/index.ts | 17 ++ .../src/ingestion/CatalogRules.ts | 45 ++++ 16 files changed, 840 insertions(+), 11 deletions(-) create mode 100644 .changeset/lazy-zoos-move.md create mode 100644 .changeset/perfect-mangos-allow.md create mode 100644 plugins/catalog-backend-module-gitlab/config.d.ts create mode 100644 plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts create mode 100644 plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts create mode 100644 plugins/catalog-backend-module-gitlab/src/providers/config.test.ts create mode 100644 plugins/catalog-backend-module-gitlab/src/providers/config.ts create mode 100644 plugins/catalog-backend-module-gitlab/src/providers/index.ts diff --git a/.changeset/lazy-zoos-move.md b/.changeset/lazy-zoos-move.md new file mode 100644 index 0000000000..1e94ec798f --- /dev/null +++ b/.changeset/lazy-zoos-move.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Add a new provider `GitlabDiscoveryEntityProvider` as replacement for `GitlabDiscoveryProcessor` diff --git a/.changeset/perfect-mangos-allow.md b/.changeset/perfect-mangos-allow.md new file mode 100644 index 0000000000..0fe75e009c --- /dev/null +++ b/.changeset/perfect-mangos-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add support to custom rules for `GitlabDiscoveryEntityProvider` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 3715b89920..986c2aec15 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -6,14 +6,58 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from repositories in GitLab --- -The GitLab integration has a special discovery processor for discovering catalog -entities from GitLab. The processor will crawl the GitLab instance and register -entities matching the configured path. This can be useful as an alternative to +The GitLab integration has a special entity provider for discovering catalog +entities from GitLab. The entity provider will crawl the GitLab instance and register +entities matching the configured paths. This can be useful as an alternative to static locations or manually adding things to the catalog. -To use the discovery processor, you'll need a GitLab integration -[set up](locations.md) with a `token`. Then you can add a location target to the -catalog configuration: +To use the discovery provider, you'll need a GitLab integration +[set up](locations.md) with a `token`. Then you can add a provider config per group +to the catalog configuration: + +```yaml +catalog: + providers: + gitlab: + yourProviderId: + host: gitlab-host # Identifies one of the hosts set up in the integrations + branch: main # Optional. Uses `master` as default + group: example-group # Group and subgroup (if needed) to look for repositories + entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` + rules: + - repository: example-repo + allow: [Component, System, Location, Template] +``` + +As this provider is not one of the default providers, you will first need to install +the gitlab catalog plugin: + +```bash +# From the Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gitlab +``` + +Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: + +```ts +/* packages/backend/src/plugins/catalog.ts */ + +import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + ...GitlabDiscoveryEntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }), + }), +); +``` + +## Alternative processor ```yaml catalog: @@ -22,6 +66,9 @@ catalog: target: https://gitlab.com/group/subgroup/blob/main/catalog-info.yaml ``` +As alternative to the entity provider `GitlabDiscoveryEntityProvider` +you can still use the `GitLabDiscoveryProcessor`. + Note the `gitlab-discovery` type, as this is not a regular `url` processor. The target is composed of three parts: diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts new file mode 100644 index 0000000000..11c6830e63 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + catalog?: { + /** + * List of provider-specific options and attributes + */ + providers?: { + /** + * GitlabDiscoveryEntityProvider configuration + * + * Uses "default" as default id for the single config variant. + */ + gitlab?: Record< + string, + { + /** + * (Required) Gitlab's host name. + * @visibility backend + */ + host: string; + /** + * (Required) Gitlab's group[/subgroup] where the discovery is done. + * @visibility backend + */ + group: string; + /** + * (Optional) Default branch to read the catalog-info.yaml file. + * If not set, 'master' will be used. + * @visibility backend + */ + branch?: string; + /** + * (Optional) The name used for the catalog file. + * If not set, 'catalog-info.yaml' will be used. + * @visibility backend + */ + entityFilename?: string; + } + >; + }; + }; +} diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index d7909bf3b3..74615e83fe 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -35,6 +35,7 @@ "dependencies": { "@backstage/backend-common": "^0.14.0-next.2", "@backstage/catalog-model": "^1.0.3-next.0", + "@backstage/backend-tasks": "^0.3.1", "@backstage/config": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/integration": "^1.2.1-next.2", @@ -43,12 +44,14 @@ "lodash": "^4.17.21", "msw": "^0.42.0", "node-fetch": "^2.6.7", - "winston": "^3.2.1" + "winston": "^3.2.1", + "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.25-next.2", - "@backstage/cli": "^0.17.2-next.2", - "@types/lodash": "^4.14.151" + "@backstage/backend-test-utils": "^0.1.25-next.1", + "@backstage/cli": "^0.17.2-next.1", + "@types/lodash": "^4.14.151", + "@types/uuid": "^8.0.0" }, "files": [ "dist" diff --git a/plugins/catalog-backend-module-gitlab/src/index.ts b/plugins/catalog-backend-module-gitlab/src/index.ts index e2a08509fb..3182210c81 100644 --- a/plugins/catalog-backend-module-gitlab/src/index.ts +++ b/plugins/catalog-backend-module-gitlab/src/index.ts @@ -21,3 +21,4 @@ */ export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; +export { GitlabDiscoveryEntityProvider } from './providers'; diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index e50ad34734..132dd0bc34 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -103,6 +103,21 @@ function setupFakeInstanceProjectsEndpoint( ); } +function setupFakeHasFileEndpoint(srv: SetupServerApi, apiBaseUrl: string) { + srv.use( + rest.head( + `${apiBaseUrl}/projects/group%2Frepo/repository/files/catalog-info.yaml`, + (req, res, ctx) => { + const branch = req.url.searchParams.get('ref'); + if (branch === 'master') { + return res(ctx.status(200)); + } + return res(ctx.status(404, 'Not Found')); + }, + ), + ); +} + describe('GitLabClient', () => { describe('isSelfManaged', () => { it('returns true if self managed instance', () => { @@ -266,3 +281,33 @@ describe('paginated', () => { expect(allItems).toHaveLength(4); }); }); + +describe('hasFile', () => { + let client: GitLabClient; + + beforeEach(() => { + setupFakeHasFileEndpoint(server, MOCK_CONFIG.apiBaseUrl); + client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + }); + + it('should not find catalog file', async () => { + const hasFile = await client.hasFile( + 'group/repo', + 'master', + 'catalog-info.yaml', + ); + expect(hasFile).toBe(true); + }); + + it('should find catalog file', async () => { + const hasFile = await client.hasFile( + 'group/repo', + 'unknown', + 'catalog-info.yaml', + ); + expect(hasFile).toBe(false); + }); +}); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 27214f3be8..25384f3dd6 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -63,6 +63,14 @@ export class GitLabClient { return this.pagedRequest(`/projects`, options); } + /** + * Checks if the catalog file is present in the repository or not. + * + * @param projectPath The path to the project + * @param branch The branch used to injest entities to the catalog + * @param filePath The path to the catalog file + * @returns `true` if the file exists, `false` otherwise + */ async hasFile( projectPath: string, branch: string, diff --git a/plugins/catalog-backend-module-gitlab/src/lib/index.ts b/plugins/catalog-backend-module-gitlab/src/lib/index.ts index 1df3d2cb84..53fad07994 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/index.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/index.ts @@ -15,4 +15,9 @@ */ export { GitLabClient, paginated } from './client'; -export type { GitLabProject } from './types'; +export type { + GitLabProject, + GitlabProviderConfig, + GitlabGroupDescription, +} from './types'; +export { readGitlabConfigs } from '../providers/config'; diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index d41ed73aac..fa2350487f 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -14,10 +14,25 @@ * limitations under the License. */ +export type GitlabGroupDescription = { + id: number; + web_url: string; + projects: GitLabProject[]; +}; + export type GitLabProject = { id: number; default_branch?: string; archived: boolean; last_activity_at: string; web_url: string; + path_with_namespace: string; +}; + +export type GitlabProviderConfig = { + host: string; + group: string; + id: string; + branch: string; + catalogFile: string; }; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts new file mode 100644 index 0000000000..dc70772d3d --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -0,0 +1,230 @@ +/* + * 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. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; +import { ConfigReader } from '@backstage/config'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { GitlabDiscoveryEntityProvider } from './GitlabDiscoveryEntityProvider'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +class PersistingTaskRunner implements TaskRunner { + private tasks: TaskInvocationDefinition[] = []; + + getTasks() { + return this.tasks; + } + + run(task: TaskInvocationDefinition): Promise { + this.tasks.push(task); + return Promise.resolve(undefined); + } +} + +const logger = getVoidLogger(); + +const server = setupServer(); + +describe('GitlabDiscoveryEntityProvider', () => { + setupRequestMockHandlers(server); + afterEach(() => jest.resetAllMocks()); + + it('no provider config', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({}); + const providers = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(0); + }); + + it('single simple discovery config', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + }, + }, + }, + }, + }); + const providers = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(1); + expect(providers[0].getProviderName()).toEqual( + 'GitlabDiscoveryEntityProvider:test-id', + ); + }); + + it('multiple discovery configs', () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + }, + 'second-test': { + host: 'test-gitlab', + group: 'second-group', + }, + }, + }, + }, + }); + const providers = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + }); + + expect(providers).toHaveLength(2); + expect(providers[0].getProviderName()).toEqual( + 'GitlabDiscoveryEntityProvider:test-id', + ); + expect(providers[1].getProviderName()).toEqual( + 'GitlabDiscoveryEntityProvider:second-test', + ); + }); + + it('apply full update on scheduled execution', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + group: 'test-group', + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabDiscoveryEntityProvider:test-id', + ); + + server.use( + rest.get( + `https://api.gitlab.example/api/v4/groups/test-group/projects`, + (_req, res, ctx) => { + const response = [ + { + id: 123, + default_branch: 'master', + archived: false, + last_activity_at: new Date().toString(), + web_url: 'https://api.gitlab.example/test-group/test-repo', + path_with_namespace: 'test-group/test-repo', + }, + ]; + return res(ctx.json(response)); + }, + ), + rest.head( + 'https://api.gitlab.example/api/v4/projects/test-group%2Ftest-repo/repository/files/catalog-info.yaml', + (req, res, ctx) => { + if (req.url.searchParams.get('ref') === 'master') { + return res(ctx.status(200)); + } + return res(ctx.status(404, 'Not Found')); + }, + ), + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual('GitlabDiscoveryEntityProvider:test-id:refresh'); + await (taskDef.fn as () => Promise)(); + + const url = `https://api.gitlab.example/test-group/test-repo/-/blob/master/catalog-info.yaml`; + const expectedEntities = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + }, + name: 'generated-cd37bf72a2fe92603f4255d9f49c6c1ead746a48', + }, + spec: { + presence: 'optional', + target: `${url}`, + type: 'url', + }, + }, + locationKey: 'GitlabDiscoveryEntityProvider:test-id', + }, + ]; + + expect(entityProviderConnection.applyMutation).toBeCalledTimes(1); + expect(entityProviderConnection.applyMutation).toBeCalledWith({ + type: 'full', + entities: expectedEntities, + }); + }); +}); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts new file mode 100644 index 0000000000..7a30aa83da --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -0,0 +1,190 @@ +/* + * 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 { TaskRunner } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { GitLabIntegration, ScmIntegrations } from '@backstage/integration'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { + GitLabClient, + GitLabProject, + GitlabProviderConfig, + paginated, + readGitlabConfigs, +} from '../lib'; +import * as uuid from 'uuid'; +import { locationSpecToLocationEntity } from '@backstage/plugin-catalog-backend'; + +type Result = { + scanned: number; + matches: GitLabProject[]; +}; + +/** + * Extracts repositories out of an GitLab instance. + * @public + */ +export class GitlabDiscoveryEntityProvider implements EntityProvider { + private readonly config: GitlabProviderConfig; + private readonly integration: GitLabIntegration; + private readonly logger: Logger; + private readonly scheduleFn: () => Promise; + private connection?: EntityProviderConnection; + + static fromConfig( + config: Config, + options: { logger: Logger; schedule: TaskRunner }, + ): GitlabDiscoveryEntityProvider[] { + const providerConfigs = readGitlabConfigs(config); + const integrations = ScmIntegrations.fromConfig(config).gitlab; + const providers: GitlabDiscoveryEntityProvider[] = []; + + providerConfigs.forEach(providerConfig => { + const integration = integrations.byHost(providerConfig.host); + if (!integration) { + throw new Error( + `No gitlab integration found that matches host ${providerConfig.host}`, + ); + } + providers.push( + new GitlabDiscoveryEntityProvider({ + ...options, + config: providerConfig, + integration, + }), + ); + }); + return providers; + } + + private constructor(options: { + config: GitlabProviderConfig; + integration: GitLabIntegration; + logger: Logger; + schedule: TaskRunner; + }) { + this.config = options.config; + this.integration = options.integration; + this.logger = options.logger.child({ + target: this.getProviderName(), + }); + this.scheduleFn = this.createScheduleFn(options.schedule); + } + + getProviderName(): string { + return `GitlabDiscoveryEntityProvider:${this.config.id}`; + } + + async connect(connection: EntityProviderConnection): Promise { + this.connection = connection; + await this.scheduleFn(); + } + + private createScheduleFn(schedule: TaskRunner): () => Promise { + return async () => { + const taskId = `${this.getProviderName()}:refresh`; + return schedule.run({ + id: taskId, + fn: async () => { + const logger = this.logger.child({ + class: GitlabDiscoveryEntityProvider.prototype.constructor.name, + taskId, + taskInstanceId: uuid.v4(), + }); + + try { + await this.refresh(logger); + } catch (error) { + logger.error(error); + } + }, + }); + }; + } + + async refresh(logger: Logger): Promise { + if (!this.connection) { + throw new Error( + `Gitlab discovery connection not initialized for ${this.getProviderName()}`, + ); + } + + const client = new GitLabClient({ + config: this.integration.config, + logger: logger, + }); + + const projects = paginated( + options => client.listProjects(options), + { + group: this.config.group, + page: 1, + per_page: 50, + }, + ); + + const res: Result = { + scanned: 0, + matches: [], + }; + + for await (const project of projects) { + res.scanned++; + + if (project.archived) { + continue; + } + + if (this.config.branch === '*' && project.default_branch === undefined) { + continue; + } + + const project_branch = project.default_branch ?? this.config.branch; + + const projectHasFile: boolean = await client.hasFile( + project.path_with_namespace, + project_branch, + this.config.catalogFile, + ); + if (projectHasFile) { + res.matches.push(project); + } + } + + const locations = res.matches.map(p => this.createLocationSpec(p)); + await this.connection.applyMutation({ + type: 'full', + entities: locations.map(location => ({ + locationKey: this.getProviderName(), + entity: locationSpecToLocationEntity({ location }), + })), + }); + } + + private createLocationSpec(project: GitLabProject): LocationSpec { + const project_branch = project.default_branch ?? this.config.branch; + return { + type: 'url', + target: `${project.web_url}/-/blob/${project_branch}/${this.config.catalogFile}`, + presence: 'optional', + }; + } +} diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts new file mode 100644 index 0000000000..d28930b151 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { readGitlabConfigs } from './config'; + +describe('config', () => { + it('empty gitlab config', () => { + const config = new ConfigReader({ + catalog: { + providers: {}, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(0); + }); + + it('valid config with default optional params', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + test: { + group: 'group', + host: 'host', + }, + }, + }, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(1); + result.forEach(r => + expect(r).toStrictEqual({ + id: 'test', + group: 'group', + branch: 'master', + host: 'host', + catalogFile: 'catalog-info.yaml', + }), + ); + }); + + it('valid config with custom optional params', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + test: { + group: 'group', + host: 'host', + branch: 'not-master', + entityFilename: 'custom-file.yaml', + }, + }, + }, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(1); + result.forEach(r => + expect(r).toStrictEqual({ + id: 'test', + group: 'group', + branch: 'not-master', + host: 'host', + catalogFile: 'custom-file.yaml', + }), + ); + }); + + it('missing params', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + test: { + branch: 'not-master', + entityFilename: 'custom-file.yaml', + }, + }, + }, + }, + }); + + expect(() => readGitlabConfigs(config)).toThrow( + "Missing required config value at 'catalog.providers.gitlab.test.group'", + ); + }); +}); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts new file mode 100644 index 0000000000..8af3d576a7 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +import { Config } from '@backstage/config'; +import { GitlabProviderConfig } from '../lib/types'; + +function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { + const group = config.getString('group'); + const host = config.getString('host'); + const branch = config.getOptionalString('branch') ?? 'master'; + const catalogFile = + config.getOptionalString('entityFilename') ?? 'catalog-info.yaml'; + + return { + id, + group, + branch, + host, + catalogFile, + }; +} + +export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { + const configs: GitlabProviderConfig[] = []; + + const providerConfigs = config.getOptionalConfig('catalog.providers.gitlab'); + + if (!providerConfigs) { + return configs; + } + + for (const id of providerConfigs.keys()) { + configs.push(readGitlabConfig(id, providerConfigs.getConfig(id))); + } + + return configs; +} diff --git a/plugins/catalog-backend-module-gitlab/src/providers/index.ts b/plugins/catalog-backend-module-gitlab/src/providers/index.ts new file mode 100644 index 0000000000..e7cb00a73f --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/providers/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { GitlabDiscoveryEntityProvider } from './GitlabDiscoveryEntityProvider'; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 587a0bd7d5..72eae22b11 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -18,6 +18,7 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import path from 'path'; import { LocationSpec } from '../api'; +import { ScmIntegrations } from '@backstage/integration'; /** * Rules to apply to catalog entities. @@ -119,6 +120,18 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { rules.push(...locationRules); } + if (config.has('catalog.providers')) { + const providersConf = config.getConfig('catalog.providers'); + const providerList = providersConf.keys(); + providerList.forEach(provider => { + if (provider === 'gitlab') { + rules.push( + ...getGitlabRules(config, providersConf.getConfig(provider)), + ); + } + }); + } + return new DefaultCatalogRulesEnforcer(rules); } @@ -187,3 +200,35 @@ function resolveTarget(type: string, target: string): string { return path.resolve(target); } + +function getGitlabRules(initialConfig: Config, config: Config): CatalogRule[] { + return config.keys().flatMap(id => { + const gitlabConf = config.getConfig(id); + if (!gitlabConf.has('rules')) { + return []; + } + + const integrations = ScmIntegrations.fromConfig(initialConfig).gitlab; + const gitlabHost = gitlabConf.getString('host'); + const integration = integrations.byHost(gitlabHost); + if (!integration) { + throw new Error( + `No gitlab integration found that matches host ${gitlabHost}`, + ); + } + + const type = `url`; + const branch = gitlabConf.getOptionalString('branch') ?? 'master'; + const entityFilename = + gitlabConf.getOptionalString('entityFilename') ?? 'catalog-info.yaml'; + + return gitlabConf.getConfigArray('rules').map(ruleConf => { + const repoName = ruleConf.getString('repository'); + const target = `${integration.config.baseUrl}/${id}/${repoName}/-/blob/${branch}/${entityFilename}`; + return { + allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), + locations: [{ type, target }], + }; + }); + }); +} From 459efebf87bdce2ae17e269608b6af7c227169a0 Mon Sep 17 00:00:00 2001 From: ivgo Date: Tue, 7 Jun 2022 08:48:49 +0200 Subject: [PATCH 10/54] Update api-report Signed-off-by: ivgo --- .../api-report.md | 21 ++++ .../src/providers/config.ts | 14 +++ yarn.lock | 100 ++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/plugins/catalog-backend-module-gitlab/api-report.md b/plugins/catalog-backend-module-gitlab/api-report.md index 63e6fe7ccb..1009da8209 100644 --- a/plugins/catalog-backend-module-gitlab/api-report.md +++ b/plugins/catalog-backend-module-gitlab/api-report.md @@ -6,8 +6,29 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; +import { TaskRunner } from '@backstage/backend-tasks'; + +// @public +export class GitlabDiscoveryEntityProvider implements EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): GitlabDiscoveryEntityProvider[]; + // (undocumented) + getProviderName(): string; + // (undocumented) + refresh(logger: Logger): Promise; +} // @public export class GitLabDiscoveryProcessor implements CatalogProcessor { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index 8af3d576a7..f7eb316997 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -17,6 +17,13 @@ import { Config } from '@backstage/config'; import { GitlabProviderConfig } from '../lib/types'; +/** + * Extracts the gitlab config from a config object + * + * @public + * + * @param config - The config object to extract from + */ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { const group = config.getString('group'); const host = config.getString('host'); @@ -33,6 +40,13 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { }; } +/** + * Extracts the gitlab config from a config object array + * + * @public + * + * @param config - The config object to extract from + */ export function readGitlabConfigs(config: Config): GitlabProviderConfig[] { const configs: GitlabProviderConfig[] = []; diff --git a/yarn.lock b/yarn.lock index c7795b8004..fb4b8f867c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1465,6 +1465,78 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@backstage/backend-common@^0.13.3": + version "0.13.5" + resolved "https://registry.npmjs.org/@backstage/backend-common/-/backend-common-0.13.5.tgz#9ff55e1da2a9cf65d0c4782cb360f9b6abd0754c" + integrity sha512-37kLw1BolfstantREuawbyRKXD+xKHUta3A63j0868JZPxRSCfiysBzF3uAorH/3WIh0p2lswutmunbVQFsXDA== + dependencies: + "@backstage/cli-common" "^0.1.9" + "@backstage/config" "^1.0.1" + "@backstage/config-loader" "^1.1.1" + "@backstage/errors" "^1.0.0" + "@backstage/integration" "^1.2.0" + "@backstage/types" "^1.0.0" + "@google-cloud/storage" "^5.8.0" + "@keyv/redis" "^2.2.3" + "@manypkg/get-packages" "^1.1.3" + "@octokit/rest" "^18.5.3" + "@types/cors" "^2.8.6" + "@types/dockerode" "^3.3.0" + "@types/express" "^4.17.6" + "@types/luxon" "^2.0.4" + "@types/webpack-env" "^1.15.2" + archiver "^5.0.2" + aws-sdk "^2.840.0" + base64-stream "^1.0.0" + compression "^1.7.4" + concat-stream "^2.0.0" + cors "^2.8.5" + dockerode "^3.3.1" + express "^4.17.1" + express-promise-router "^4.1.0" + fs-extra "10.1.0" + git-url-parse "^11.6.0" + helmet "^5.0.2" + isomorphic-git "^1.8.0" + jose "^4.6.0" + keyv "^4.0.3" + keyv-memcache "^1.2.5" + knex "^1.0.2" + lodash "^4.17.21" + logform "^2.3.2" + luxon "^2.3.1" + minimatch "^5.0.0" + minimist "^1.2.5" + morgan "^1.10.0" + node-abort-controller "^3.0.1" + node-fetch "^2.6.7" + raw-body "^2.4.1" + selfsigned "^2.0.0" + stoppable "^1.1.0" + tar "^6.1.2" + unzipper "^0.10.11" + winston "^3.2.1" + yn "^4.0.0" + +"@backstage/backend-tasks@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/backend-tasks/-/backend-tasks-0.3.1.tgz#551ff2f5eb41ea1af21c166f957a24512f87909b" + integrity sha512-aCUJl2E0lrmIyzSLKP8kKSZVBA8Wb6cOiHVmoYVrOe9VOvPDx4DZtnvL2iDF+bKfuQlWsvKoAQOcBlgJfyeLeg== + dependencies: + "@backstage/backend-common" "^0.13.3" + "@backstage/config" "^1.0.1" + "@backstage/errors" "^1.0.0" + "@backstage/types" "^1.0.0" + "@types/luxon" "^2.0.4" + cron "^1.8.2" + knex "^1.0.2" + lodash "^4.17.21" + luxon "^2.0.2" + node-abort-controller "^3.0.1" + uuid "^8.0.0" + winston "^3.2.1" + zod "^3.9.5" + "@backstage/catalog-client@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-1.0.2.tgz#8450bb09e6e65114053538d1ab855bc8bee09531" @@ -1487,6 +1559,27 @@ lodash "^4.17.21" uuid "^8.0.0" +"@backstage/config-loader@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@backstage/config-loader/-/config-loader-1.1.1.tgz#552bbf331acdee19247f158e62b20649cf07f427" + integrity sha512-LYmX+BPMn74Pyi/tDiELNNDCyKkJIsQL/PKbNw15CEd6LDJI5jOXY9pMxBPlbpg1fYyA46AeF0Yu6V5s/GXa7Q== + dependencies: + "@backstage/cli-common" "^0.1.9" + "@backstage/config" "^1.0.1" + "@backstage/errors" "^1.0.0" + "@backstage/types" "^1.0.0" + "@types/json-schema" "^7.0.6" + ajv "^8.10.0" + chokidar "^3.5.2" + fs-extra "10.1.0" + json-schema "^0.4.0" + json-schema-merge-allof "^0.8.1" + json-schema-traverse "^1.0.0" + node-fetch "^2.6.7" + typescript-json-schema "^0.53.0" + yaml "^1.9.2" + yup "^0.32.9" + "@backstage/core-components@^0.9.0", "@backstage/core-components@^0.9.4": version "0.9.4" resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.9.4.tgz#47e9a305f768a951e0cb0ffa9c1e3c141d06b223" @@ -10283,6 +10376,13 @@ crelt@^1.0.5: resolved "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94" integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA== +cron@^1.8.2: + version "1.8.3" + resolved "https://registry.npmjs.org/cron/-/cron-1.8.3.tgz#2a61d7b15848716885834ec56ac072f4b9744ebd" + integrity sha512-JYR/QZFklJCIPndBLfd/2nU1nSlCMrUdtQ2mGLXSVM/qqqEK7DOrFR0gsEiyeqs0PdWrs0ve1ggH4V7XksDwXg== + dependencies: + luxon "^1.23.x" + cron@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/cron/-/cron-2.0.0.tgz#15c6bf37c1cebf6da1d7a688b9ba1c68338bfe6b" From 58927c37841f3024b1c66ec675018aff0f1a0527 Mon Sep 17 00:00:00 2001 From: ivgo Date: Tue, 7 Jun 2022 09:18:40 +0200 Subject: [PATCH 11/54] Fix local dependency Signed-off-by: ivgo --- plugins/catalog-backend-module-gitlab/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 74615e83fe..048ce85cb8 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -35,7 +35,7 @@ "dependencies": { "@backstage/backend-common": "^0.14.0-next.2", "@backstage/catalog-model": "^1.0.3-next.0", - "@backstage/backend-tasks": "^0.3.1", + "@backstage/backend-tasks": "^0.3.2-next.1", "@backstage/config": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/integration": "^1.2.1-next.2", From f96cdcb9f0b1f993ef72e8f46a7d3913589bb470 Mon Sep 17 00:00:00 2001 From: ivgo Date: Tue, 7 Jun 2022 11:49:09 +0200 Subject: [PATCH 12/54] Fix typo and add more explanation in the changeset Signed-off-by: ivgo --- .changeset/lazy-zoos-move.md | 62 +++++++++++++++++++++++++++ docs/integrations/gitlab/discovery.md | 4 +- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/.changeset/lazy-zoos-move.md b/.changeset/lazy-zoos-move.md index 1e94ec798f..4bd78cee1d 100644 --- a/.changeset/lazy-zoos-move.md +++ b/.changeset/lazy-zoos-move.md @@ -3,3 +3,65 @@ --- Add a new provider `GitlabDiscoveryEntityProvider` as replacement for `GitlabDiscoveryProcessor` + +In order to migrate from the `GitlabDiscoveryProcessor` you need to apply +the following changes: + +**Before:** + +```yaml +# app-config.yaml + +catalog: + locations: + - type: gitlab-discovery + target: https://company.gitlab.com/prefix/*/catalog-info.yaml +``` + +```ts +/* packages/backend/src/plugins/catalog.ts */ + +import { GitlabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors ... */ +builder.addProcessor( + GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }), +); +``` + +**After:** + +```yaml +# app-config.yaml + +catalog: + providers: + gitlab: + yourProviderId: # identifies your dataset / provider independent of config changes + host: gitlab-host # Identifies one of the hosts set up in the integrations + branch: main # Optional. Uses `master` as default + group: example-group # Group and subgroup (if needed) to look for repositories + entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` + rules: # Optional. Uses the default rules if not present + - repository: example-repo + allow: [Component, System, Location, Template] +``` + +```ts +/* packages/backend/src/plugins/catalog.ts */ + +import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + ...GitlabDiscoveryEntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }), + }), +); +``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 986c2aec15..6383e70e64 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -24,7 +24,7 @@ catalog: branch: main # Optional. Uses `master` as default group: example-group # Group and subgroup (if needed) to look for repositories entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` - rules: + rules: # Optional. Uses the default rules if not present - repository: example-repo allow: [Component, System, Location, Template] ``` @@ -42,7 +42,7 @@ Once you've done that, you'll also need to add the segment below to `packages/ba ```ts /* packages/backend/src/plugins/catalog.ts */ -import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; +import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab'; const builder = await CatalogBuilder.create(env); /** ... other processors and/or providers ... */ From 90450c925b18a14705abf8545b60e3c3776c83a8 Mon Sep 17 00:00:00 2001 From: ivgo Date: Tue, 7 Jun 2022 17:34:23 +0200 Subject: [PATCH 13/54] Fix some typos & update yarn.lock after release Signed-off-by: ivgo --- .../catalog-backend-module-gitlab/config.d.ts | 2 - .../src/lib/client.ts | 9 +- .../GitlabDiscoveryEntityProvider.ts | 2 +- yarn.lock | 100 ------------------ 4 files changed, 5 insertions(+), 108 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 11c6830e63..5b0d42c81d 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -22,8 +22,6 @@ export interface Config { providers?: { /** * GitlabDiscoveryEntityProvider configuration - * - * Uses "default" as default id for the single config variant. */ gitlab?: Record< string, diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 25384f3dd6..adf0a78e8f 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -64,12 +64,11 @@ export class GitLabClient { } /** - * Checks if the catalog file is present in the repository or not. + * General existence check. * - * @param projectPath The path to the project - * @param branch The branch used to injest entities to the catalog - * @param filePath The path to the catalog file - * @returns `true` if the file exists, `false` otherwise + * @param projectPath - The path to the project + * @param branch - The branch used to search + * @param filePath - The path to the file */ async hasFile( projectPath: string, diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 7a30aa83da..1ecf274c3b 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -39,7 +39,7 @@ type Result = { }; /** - * Extracts repositories out of an GitLab instance. + * Discovers entity definition files in the groups of a Gitlab instance. * @public */ export class GitlabDiscoveryEntityProvider implements EntityProvider { diff --git a/yarn.lock b/yarn.lock index fb4b8f867c..c7795b8004 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1465,78 +1465,6 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@backstage/backend-common@^0.13.3": - version "0.13.5" - resolved "https://registry.npmjs.org/@backstage/backend-common/-/backend-common-0.13.5.tgz#9ff55e1da2a9cf65d0c4782cb360f9b6abd0754c" - integrity sha512-37kLw1BolfstantREuawbyRKXD+xKHUta3A63j0868JZPxRSCfiysBzF3uAorH/3WIh0p2lswutmunbVQFsXDA== - dependencies: - "@backstage/cli-common" "^0.1.9" - "@backstage/config" "^1.0.1" - "@backstage/config-loader" "^1.1.1" - "@backstage/errors" "^1.0.0" - "@backstage/integration" "^1.2.0" - "@backstage/types" "^1.0.0" - "@google-cloud/storage" "^5.8.0" - "@keyv/redis" "^2.2.3" - "@manypkg/get-packages" "^1.1.3" - "@octokit/rest" "^18.5.3" - "@types/cors" "^2.8.6" - "@types/dockerode" "^3.3.0" - "@types/express" "^4.17.6" - "@types/luxon" "^2.0.4" - "@types/webpack-env" "^1.15.2" - archiver "^5.0.2" - aws-sdk "^2.840.0" - base64-stream "^1.0.0" - compression "^1.7.4" - concat-stream "^2.0.0" - cors "^2.8.5" - dockerode "^3.3.1" - express "^4.17.1" - express-promise-router "^4.1.0" - fs-extra "10.1.0" - git-url-parse "^11.6.0" - helmet "^5.0.2" - isomorphic-git "^1.8.0" - jose "^4.6.0" - keyv "^4.0.3" - keyv-memcache "^1.2.5" - knex "^1.0.2" - lodash "^4.17.21" - logform "^2.3.2" - luxon "^2.3.1" - minimatch "^5.0.0" - minimist "^1.2.5" - morgan "^1.10.0" - node-abort-controller "^3.0.1" - node-fetch "^2.6.7" - raw-body "^2.4.1" - selfsigned "^2.0.0" - stoppable "^1.1.0" - tar "^6.1.2" - unzipper "^0.10.11" - winston "^3.2.1" - yn "^4.0.0" - -"@backstage/backend-tasks@^0.3.1": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/backend-tasks/-/backend-tasks-0.3.1.tgz#551ff2f5eb41ea1af21c166f957a24512f87909b" - integrity sha512-aCUJl2E0lrmIyzSLKP8kKSZVBA8Wb6cOiHVmoYVrOe9VOvPDx4DZtnvL2iDF+bKfuQlWsvKoAQOcBlgJfyeLeg== - dependencies: - "@backstage/backend-common" "^0.13.3" - "@backstage/config" "^1.0.1" - "@backstage/errors" "^1.0.0" - "@backstage/types" "^1.0.0" - "@types/luxon" "^2.0.4" - cron "^1.8.2" - knex "^1.0.2" - lodash "^4.17.21" - luxon "^2.0.2" - node-abort-controller "^3.0.1" - uuid "^8.0.0" - winston "^3.2.1" - zod "^3.9.5" - "@backstage/catalog-client@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-1.0.2.tgz#8450bb09e6e65114053538d1ab855bc8bee09531" @@ -1559,27 +1487,6 @@ lodash "^4.17.21" uuid "^8.0.0" -"@backstage/config-loader@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@backstage/config-loader/-/config-loader-1.1.1.tgz#552bbf331acdee19247f158e62b20649cf07f427" - integrity sha512-LYmX+BPMn74Pyi/tDiELNNDCyKkJIsQL/PKbNw15CEd6LDJI5jOXY9pMxBPlbpg1fYyA46AeF0Yu6V5s/GXa7Q== - dependencies: - "@backstage/cli-common" "^0.1.9" - "@backstage/config" "^1.0.1" - "@backstage/errors" "^1.0.0" - "@backstage/types" "^1.0.0" - "@types/json-schema" "^7.0.6" - ajv "^8.10.0" - chokidar "^3.5.2" - fs-extra "10.1.0" - json-schema "^0.4.0" - json-schema-merge-allof "^0.8.1" - json-schema-traverse "^1.0.0" - node-fetch "^2.6.7" - typescript-json-schema "^0.53.0" - yaml "^1.9.2" - yup "^0.32.9" - "@backstage/core-components@^0.9.0", "@backstage/core-components@^0.9.4": version "0.9.4" resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.9.4.tgz#47e9a305f768a951e0cb0ffa9c1e3c141d06b223" @@ -10376,13 +10283,6 @@ crelt@^1.0.5: resolved "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94" integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA== -cron@^1.8.2: - version "1.8.3" - resolved "https://registry.npmjs.org/cron/-/cron-1.8.3.tgz#2a61d7b15848716885834ec56ac072f4b9744ebd" - integrity sha512-JYR/QZFklJCIPndBLfd/2nU1nSlCMrUdtQ2mGLXSVM/qqqEK7DOrFR0gsEiyeqs0PdWrs0ve1ggH4V7XksDwXg== - dependencies: - luxon "^1.23.x" - cron@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/cron/-/cron-2.0.0.tgz#15c6bf37c1cebf6da1d7a688b9ba1c68338bfe6b" From 6424c0ad6fd7be5af38af30fa9680fbbfe54d58d Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 14:51:07 +0200 Subject: [PATCH 14/54] chore: give alternative to deprecated projectId Signed-off-by: djamaile --- .../scaffolder/actions/builtin/publish/gitlabMergeRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index c5dd41f807..8c2e9efeb2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -185,7 +185,7 @@ export const createPublishGitlabMergeRequestAction = (options: { ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); - /** @deprecated Use projectPath instead */ + /** @deprecated */ ctx.output('projectid', projectPath); ctx.output('projectPath', projectPath); ctx.output('mergeRequestUrl', mergeRequestUrl); From 6b1ed2292d4d01fcb13a99faccc522de1be370bc Mon Sep 17 00:00:00 2001 From: ivgo Date: Thu, 9 Jun 2022 14:50:01 +0200 Subject: [PATCH 15/54] Remove rules & update some tsc errors Signed-off-by: ivgo --- .changeset/lazy-zoos-move.md | 3 -- .changeset/perfect-mangos-allow.md | 5 --- docs/integrations/gitlab/discovery.md | 3 -- .../src/lib/types.ts | 2 +- .../GitlabDiscoveryEntityProvider.ts | 2 +- .../src/ingestion/CatalogRules.ts | 45 ------------------- 6 files changed, 2 insertions(+), 58 deletions(-) delete mode 100644 .changeset/perfect-mangos-allow.md diff --git a/.changeset/lazy-zoos-move.md b/.changeset/lazy-zoos-move.md index 4bd78cee1d..69bd517438 100644 --- a/.changeset/lazy-zoos-move.md +++ b/.changeset/lazy-zoos-move.md @@ -43,9 +43,6 @@ catalog: branch: main # Optional. Uses `master` as default group: example-group # Group and subgroup (if needed) to look for repositories entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` - rules: # Optional. Uses the default rules if not present - - repository: example-repo - allow: [Component, System, Location, Template] ``` ```ts diff --git a/.changeset/perfect-mangos-allow.md b/.changeset/perfect-mangos-allow.md deleted file mode 100644 index 0fe75e009c..0000000000 --- a/.changeset/perfect-mangos-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Add support to custom rules for `GitlabDiscoveryEntityProvider` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 6383e70e64..ea151e9415 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -24,9 +24,6 @@ catalog: branch: main # Optional. Uses `master` as default group: example-group # Group and subgroup (if needed) to look for repositories entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` - rules: # Optional. Uses the default rules if not present - - repository: example-repo - allow: [Component, System, Location, Template] ``` As this provider is not one of the default providers, you will first need to install diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index fa2350487f..69de9d28c7 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -26,7 +26,7 @@ export type GitLabProject = { archived: boolean; last_activity_at: string; web_url: string; - path_with_namespace: string; + path_with_namespace?: string; }; export type GitlabProviderConfig = { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 1ecf274c3b..36c3ee90be 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -160,7 +160,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { const project_branch = project.default_branch ?? this.config.branch; const projectHasFile: boolean = await client.hasFile( - project.path_with_namespace, + project.path_with_namespace ?? '', project_branch, this.config.catalogFile, ); diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 72eae22b11..587a0bd7d5 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -18,7 +18,6 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import path from 'path'; import { LocationSpec } from '../api'; -import { ScmIntegrations } from '@backstage/integration'; /** * Rules to apply to catalog entities. @@ -120,18 +119,6 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { rules.push(...locationRules); } - if (config.has('catalog.providers')) { - const providersConf = config.getConfig('catalog.providers'); - const providerList = providersConf.keys(); - providerList.forEach(provider => { - if (provider === 'gitlab') { - rules.push( - ...getGitlabRules(config, providersConf.getConfig(provider)), - ); - } - }); - } - return new DefaultCatalogRulesEnforcer(rules); } @@ -200,35 +187,3 @@ function resolveTarget(type: string, target: string): string { return path.resolve(target); } - -function getGitlabRules(initialConfig: Config, config: Config): CatalogRule[] { - return config.keys().flatMap(id => { - const gitlabConf = config.getConfig(id); - if (!gitlabConf.has('rules')) { - return []; - } - - const integrations = ScmIntegrations.fromConfig(initialConfig).gitlab; - const gitlabHost = gitlabConf.getString('host'); - const integration = integrations.byHost(gitlabHost); - if (!integration) { - throw new Error( - `No gitlab integration found that matches host ${gitlabHost}`, - ); - } - - const type = `url`; - const branch = gitlabConf.getOptionalString('branch') ?? 'master'; - const entityFilename = - gitlabConf.getOptionalString('entityFilename') ?? 'catalog-info.yaml'; - - return gitlabConf.getConfigArray('rules').map(ruleConf => { - const repoName = ruleConf.getString('repository'); - const target = `${integration.config.baseUrl}/${id}/${repoName}/-/blob/${branch}/${entityFilename}`; - return { - allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), - locations: [{ type, target }], - }; - }); - }); -} From f780859eb0691fc254e8547eb68cddb51ceb7dbb Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jun 2022 16:12:16 +0200 Subject: [PATCH 16/54] feat: added the ability to run namespaced templates Signed-off-by: blam --- plugins/scaffolder-backend/src/service/helpers.ts | 5 ----- plugins/scaffolder/src/components/Router.tsx | 14 ++++++++++++-- .../src/components/TaskPage/TaskPage.tsx | 4 ++-- .../src/components/TemplateCard/TemplateCard.tsx | 10 ++++++++-- .../src/components/TemplatePage/TemplatePage.tsx | 15 ++++++++------- .../scaffolder/src/next/Router/Router.test.tsx | 6 ++++-- .../TemplateCard/TemplateCard.test.tsx | 2 +- .../TemplateCard/TemplateCard.tsx | 9 +++++++-- plugins/scaffolder/src/routes.ts | 8 +++++++- 9 files changed, 49 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index 23200d6aac..13197861a8 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -95,11 +95,6 @@ export async function findTemplate(options: { }): Promise { const { entityRef, token, catalogApi } = options; - if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) { - throw new InputError( - `Invalid namespace, only '${DEFAULT_NAMESPACE}' namespace is supported`, - ); - } if (entityRef.kind.toLocaleLowerCase('en-US') !== 'template') { throw new InputError(`Invalid kind, only 'Template' kind is supported`); } diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 6beaa3419a..172a21234a 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentType } from 'react'; -import { Routes, Route, useOutlet, Navigate } from 'react-router'; +import { Routes, Route, useOutlet, Navigate, useParams } from 'react-router'; import { Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; @@ -31,10 +31,11 @@ import { FIELD_EXTENSION_KEY, DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, } from '../extensions'; -import { useElementFilter } from '@backstage/core-plugin-api'; +import { useElementFilter, useRouteRef } from '@backstage/core-plugin-api'; import { actionsRouteRef, editRouteRef, + legacySelectedTemplateRouteRef, scaffolderListTaskRouteRef, scaffolderTaskRouteRef, selectedTemplateRouteRef, @@ -101,6 +102,12 @@ export const Router = (props: RouterProps) => { ), ]; + const RedirectingComponent = () => { + const { templateName } = useParams(); + const newLink = useRouteRef(selectedTemplateRouteRef); + return ; + }; + return ( { /> } /> + + + { const formData = taskStream.task!.spec.parameters; - const { name } = parseEntityRef( + const { name, namespace } = parseEntityRef( taskStream.task!.spec.templateInfo?.entityRef, ); navigate( - `${templateRoute({ templateName: name })}?${qs.stringify({ + `${templateRoute({ templateName: name, namespace })}?${qs.stringify({ formData: JSON.stringify(formData), })}`, ); diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 6f7e375cce..df46a119f2 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + Entity, + parseEntityRef, + RELATION_OWNED_BY, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrationIcon, @@ -162,7 +167,8 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { : 'other'; const theme = backstageTheme.getPageTheme({ themeId }); const classes = useStyles({ backgroundImage: theme.backgroundImage }); - const href = templateRoute({ templateName: templateProps.name }); + const { name, namespace } = parseEntityRef(stringifyEntityRef(template)); + const href = templateRoute({ templateName: name, namespace }); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); const sourceLocation = getEntitySourceLocation(template, scmIntegrationsApi); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 22f995eae4..f694688801 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -54,11 +54,16 @@ export const TemplatePage = ({ const secretsContext = useContext(SecretsContext); const errorApi = useApi(errorApiRef); const scaffolderApi = useApi(scaffolderApiRef); - const { templateName } = useParams(); + const { templateName, namespace } = useParams(); + const templateRef = stringifyEntityRef({ + name: templateName, + kind: 'template', + namespace, + }); const navigate = useNavigate(); const scaffolderTaskRoute = useRouteRef(scaffolderTaskRouteRef); const rootRoute = useRouteRef(rootRouteRef); - const { schema, loading, error } = useTemplateParameterSchema(templateName); + const { schema, loading, error } = useTemplateParameterSchema(templateRef); const [formState, setFormState] = useState>(() => { const query = qs.parse(window.location.search, { ignoreQueryPrefix: true, @@ -78,11 +83,7 @@ export const TemplatePage = ({ const handleCreate = async () => { const { taskId } = await scaffolderApi.scaffold({ - templateRef: stringifyEntityRef({ - name: templateName, - kind: 'template', - namespace: 'default', - }), + templateRef, values: formState, secrets: secretsContext?.secrets, }); diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx index 5094c786e0..1aaca1a94b 100644 --- a/plugins/scaffolder/src/next/Router/Router.test.tsx +++ b/plugins/scaffolder/src/next/Router/Router.test.tsx @@ -47,7 +47,9 @@ describe('Router', () => { describe('/templates/:templateName', () => { it('should render the TemplateWizard page', async () => { - await renderInTestApp(, { routeEntries: ['/templates/foo'] }); + await renderInTestApp(, { + routeEntries: ['/templates/default/foo'], + }); expect(TemplateWizardPage).toHaveBeenCalled(); }); @@ -67,7 +69,7 @@ describe('Router', () => { , - { routeEntries: ['/templates/foo'] }, + { routeEntries: ['/templates/default/foo'] }, ); const mock = TemplateWizardPage as jest.Mock; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx index 45f4dc6317..44e3a0bbbf 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx @@ -233,7 +233,7 @@ describe('TemplateCard', () => { expect(getByRole('button', { name: 'Choose' })).toBeInTheDocument(); expect(getByRole('button', { name: 'Choose' })).toHaveAttribute( 'href', - '/templates/bob', + '/templates/default/bob', ); }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx index 0e67ecd82a..874c20a9ac 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -26,7 +26,11 @@ import { } from '@material-ui/core'; import { CardHeader } from './CardHeader'; import { MarkdownContent, UserIcon, Button } from '@backstage/core-components'; -import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + parseEntityRef, + RELATION_OWNED_BY, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { EntityRefLinks, getEntityRelations, @@ -91,7 +95,8 @@ export const TemplateCard = (props: TemplateCardProps) => { const styles = useStyles(); const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY); const templateRoute = useRouteRef(selectedTemplateRouteRef); - const href = templateRoute({ templateName: template.metadata.name }); + const { name, namespace } = parseEntityRef(stringifyEntityRef(template)); + const href = templateRoute({ templateName: name, namespace: namespace }); return ( diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index be0d69135a..dbc54bb22c 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -28,10 +28,16 @@ export const rootRouteRef = createRouteRef({ id: 'scaffolder', }); +export const legacySelectedTemplateRouteRef = createSubRouteRef({ + id: 'scaffolder/legacy/selected-template', + parent: rootRouteRef, + path: '/templates/:templateName', +}); + export const selectedTemplateRouteRef = createSubRouteRef({ id: 'scaffolder/selected-template', parent: rootRouteRef, - path: '/templates/:templateName', + path: '/templates/:namespace/:templateName', }); export const scaffolderTaskRouteRef = createSubRouteRef({ From f93af969cd9c04dac54061cc1bc12e2ebeb488db Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jun 2022 16:15:13 +0200 Subject: [PATCH 17/54] chore: tidying up a little bit and added changeset Signed-off-by: blam --- .changeset/tender-carpets-cry.md | 6 ++++++ plugins/scaffolder/src/components/Router.tsx | 14 ++++++++++++-- plugins/scaffolder/src/routes.ts | 3 +++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .changeset/tender-carpets-cry.md diff --git a/.changeset/tender-carpets-cry.md b/.changeset/tender-carpets-cry.md new file mode 100644 index 0000000000..a3d275fe33 --- /dev/null +++ b/.changeset/tender-carpets-cry.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +--- + +Added the ability to support running of templates that are not in the `default` namespace diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 172a21234a..60dc5ed474 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React, { ComponentType, useEffect } from 'react'; import { Routes, Route, useOutlet, Navigate, useParams } from 'react-router'; import { Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; @@ -101,10 +101,20 @@ export const Router = (props: RouterProps) => { ), ), ]; - + /** + * This component can be deleted once the older routes have been deprecated. + */ const RedirectingComponent = () => { const { templateName } = useParams(); const newLink = useRouteRef(selectedTemplateRouteRef); + // eslint-disable-next-line no-console + useEffect( + () => + console.warn( + 'The route /template/:templateName is deprecated, please use the new /template/:namespace/:templateName route instead', + ), + [], + ); return ; }; diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index dbc54bb22c..8d6875e351 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -28,6 +28,9 @@ export const rootRouteRef = createRouteRef({ id: 'scaffolder', }); +/** + * @deprecated This is the old template route, can be deleted before next major release + */ export const legacySelectedTemplateRouteRef = createSubRouteRef({ id: 'scaffolder/legacy/selected-template', parent: rootRouteRef, From 0a8b21216535dd0a7e42c1dca6277c81ec387f44 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jun 2022 16:22:08 +0200 Subject: [PATCH 18/54] chore: wrong line Signed-off-by: blam --- plugins/scaffolder/src/components/Router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 60dc5ed474..ac35c4dcd7 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -107,9 +107,9 @@ export const Router = (props: RouterProps) => { const RedirectingComponent = () => { const { templateName } = useParams(); const newLink = useRouteRef(selectedTemplateRouteRef); - // eslint-disable-next-line no-console useEffect( () => + // eslint-disable-next-line no-console console.warn( 'The route /template/:templateName is deprecated, please use the new /template/:namespace/:templateName route instead', ), From 216172b0153120ad67c0e03c3b1c7fea39177f66 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 9 Jun 2022 16:41:54 +0200 Subject: [PATCH 19/54] chore: fix typescript error Signed-off-by: blam --- plugins/scaffolder-backend/src/service/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index 13197861a8..3412fec2d3 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -21,7 +21,6 @@ import { parseLocationRef, ANNOTATION_SOURCE_LOCATION, CompoundEntityRef, - DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; From 0701b2d71d7e7d9e12f04885121fb992461a1f93 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 9 Jun 2022 17:19:24 +0200 Subject: [PATCH 20/54] changesets: exit prerelease Signed-off-by: Patrik Oldsberg --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index cb5047cf20..1fce147888 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.71", From 5179b8d80516604175e638e57f2feacdef1b4db6 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 9 Jun 2022 22:48:11 +0200 Subject: [PATCH 21/54] chore: add warning to notify user that projectid is deprecated Signed-off-by: djamaile --- .../actions/builtin/publish/gitlabMergeRequest.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 8c2e9efeb2..f717a7a0c6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -111,6 +111,12 @@ export const createPublishGitlabMergeRequestAction = (options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); const projectPath = `${owner}/${repo}`; + if (ctx.input.projectid) { + const deprecationWarning = `Property "projectid" is deprecated and no longer to needed to create a MR`; + ctx.logger.warn(deprecationWarning); + console.warn(deprecationWarning); + } + const integrationConfig = integrations.gitlab.byHost(host); const destinationBranch = ctx.input.branchName; From 1b59892019bf4073cbe8251450742f6e33a64d27 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 10:03:25 +0200 Subject: [PATCH 22/54] implement possibilities to set custom template on elastic search engine Signed-off-by: Emma Indal --- .../engines/ElasticSearchSearchEngine.test.ts | 64 +++++++++++++++++++ .../src/engines/ElasticSearchSearchEngine.ts | 38 +++++++++++ 2 files changed, 102 insertions(+) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 9867920c6e..c1c8ed9c86 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -32,6 +32,9 @@ class ElasticSearchSearchEngineForTranslatorTests extends ElasticSearchSearchEng getTranslator() { return this.translator; } + getIndexTemplate() { + return this.indexTemplate; + } } const mock = new Mock(); @@ -50,6 +53,23 @@ jest.mock('./ElasticSearchSearchEngineIndexer', () => ({ .mockImplementation(() => indexerMock), })); +const customIndexTemplate = { + name: 'custom-index-template', + body: { + index_patterns: ['*'], + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + }, + }, + }, +}; + describe('ElasticSearchSearchEngine', () => { let testSearchEngine: ElasticSearchSearchEngine; let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests; @@ -72,6 +92,20 @@ describe('ElasticSearchSearchEngine', () => { client = testSearchEngine['elasticSearchClient']; }); + describe('custom index template', () => { + it('should not have custom template set as default', async () => { + expect(inspectableSearchEngine.getIndexTemplate()).toBeUndefined(); + }); + + it('should set custom index template', async () => { + await inspectableSearchEngine.setIndexTemplate(customIndexTemplate); + + expect(inspectableSearchEngine.getIndexTemplate()).toMatchObject( + customIndexTemplate, + ); + }); + }); + describe('queryTranslator', () => { beforeAll(() => { mock.clearAll(); @@ -760,6 +794,10 @@ describe('ElasticSearchSearchEngine', () => { }); describe('indexer', () => { + beforeEach(async () => { + await testSearchEngine.setIndexTemplate(customIndexTemplate); + }); + it('should get indexer', async () => { const indexer = await testSearchEngine.getIndexer('test-index'); @@ -773,12 +811,38 @@ describe('ElasticSearchSearchEngine', () => { elasticSearchClient: client, }), ); + expect(indexerMock.on).toHaveBeenCalledWith( + 'finish', + expect.any(Function), + ); expect(indexerMock.on).toHaveBeenCalledWith( 'error', expect.any(Function), ); }); + describe('onFinish', () => { + let callback: Function; + + beforeEach(async () => { + mock.clearAll(); + await testSearchEngine.getIndexer('test-index'); + callback = indexerMock.on.mock.calls[1][1]; + }); + + it('should set provided index template on finish', async () => { + const indexTemplateSpy = jest.fn().mockReturnValue(customIndexTemplate); + mock.add( + { method: 'PUT', path: '/_index_template/custom-index-template' }, + indexTemplateSpy, + ); + + await callback(); + + expect(indexTemplateSpy).toHaveBeenCalled(); + }); + }); + describe('onError', () => { let errorHandler: Function; const error = new Error('some error'); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 6541d2ed80..025a9fe1fc 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -36,6 +36,24 @@ import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineInd export type { ElasticSearchClientOptions }; +/** + * Elasticsearch specific index template + * @public + */ +export type ElasticSearchCustomIndexTemplate = { + name: string; + body: ElasticSearchIndexTemplateBody; +}; + +/** + * Elasticsearch specific index template body + * @public + */ +export type ElasticSearchIndexTemplateBody = { + index_patterns: string[]; + template: Record; +}; + /** * Search query that the elasticsearch engine understands. * @public @@ -114,6 +132,7 @@ function isBlank(str: string) { export class ElasticSearchSearchEngine implements SearchEngine { private readonly elasticSearchClient: Client; private readonly highlightOptions: ElasticSearchHighlightConfig; + protected indexTemplate?: ElasticSearchCustomIndexTemplate; constructor( private readonly elasticSearchClientOptions: ElasticSearchClientOptions, @@ -236,8 +255,13 @@ export class ElasticSearchSearchEngine implements SearchEngine { this.translator = translator; } + setIndexTemplate(template: ElasticSearchCustomIndexTemplate) { + this.indexTemplate = template; + } + async getIndexer(type: string) { const alias = this.constructSearchAlias(type); + const indexer = new ElasticSearchSearchEngineIndexer({ type, indexPrefix: this.indexPrefix, @@ -266,6 +290,20 @@ export class ElasticSearchSearchEngine implements SearchEngine { } }); + indexer.on('finish', async () => { + // Set custom index template if set + if (this.indexTemplate) { + try { + await this.elasticSearchClient.indices.putIndexTemplate( + this.indexTemplate, + ); + this.logger.info('Custom index template set'); + } catch (error) { + this.logger.error(`Unable to set custom index template: ${error}`); + } + } + }); + return indexer; } From 535847cf14d521e694177ae01464c39fa2548243 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 10:08:48 +0200 Subject: [PATCH 23/54] changeset and api report updates Signed-off-by: Emma Indal --- .changeset/wet-icons-shout.md | 5 +++++ .../api-report.md | 16 ++++++++++++++++ .../src/engines/ElasticSearchSearchEngine.ts | 6 ++++-- .../src/engines/index.ts | 2 ++ .../src/index.ts | 2 ++ 5 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .changeset/wet-icons-shout.md diff --git a/.changeset/wet-icons-shout.md b/.changeset/wet-icons-shout.md new file mode 100644 index 0000000000..de105cbde9 --- /dev/null +++ b/.changeset/wet-icons-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Now possible to set a custom index template on the elasticsearch search engine. diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 143d3ab3e8..a6be34dcb1 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -137,6 +137,18 @@ export interface ElasticSearchConnectionConstructor { }; } +// @public +export type ElasticSearchCustomIndexTemplate = { + name: string; + body: ElasticSearchCustomIndexTemplateBody; +}; + +// @public +export type ElasticSearchCustomIndexTemplateBody = { + index_patterns: string[]; + template: Record; +}; + // @public (undocumented) export type ElasticSearchHighlightConfig = { fragmentDelimiter: string; @@ -211,10 +223,14 @@ export class ElasticSearchSearchEngine implements SearchEngine { }: ElasticSearchOptions): Promise; // (undocumented) getIndexer(type: string): Promise; + // (undocumented) + protected indexTemplate?: ElasticSearchCustomIndexTemplate; newClient(create: (options: ElasticSearchClientOptions) => T): T; // (undocumented) query(query: SearchQuery): Promise; // (undocumented) + setIndexTemplate(template: ElasticSearchCustomIndexTemplate): void; + // (undocumented) setTranslator(translator: ElasticSearchQueryTranslator): void; // (undocumented) protected translator( diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 025a9fe1fc..a4b3be6eec 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -42,15 +42,17 @@ export type { ElasticSearchClientOptions }; */ export type ElasticSearchCustomIndexTemplate = { name: string; - body: ElasticSearchIndexTemplateBody; + body: ElasticSearchCustomIndexTemplateBody; }; /** * Elasticsearch specific index template body * @public */ -export type ElasticSearchIndexTemplateBody = { +export type ElasticSearchCustomIndexTemplateBody = { index_patterns: string[]; + // See available properties of template + // https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body template: Record; }; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index d99f96ef72..8ac86970a1 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -30,6 +30,8 @@ export type { ElasticSearchQueryTranslator, ElasticSearchQueryTranslatorOptions, ElasticSearchOptions, + ElasticSearchCustomIndexTemplate, + ElasticSearchCustomIndexTemplateBody, } from './ElasticSearchSearchEngine'; export type { ElasticSearchSearchEngineIndexer, diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index 772c7d3689..adf7fa43f2 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -36,4 +36,6 @@ export type { ElasticSearchAuth, ElasticSearchSearchEngineIndexer, ElasticSearchSearchEngineIndexerOptions, + ElasticSearchCustomIndexTemplate, + ElasticSearchCustomIndexTemplateBody, } from './engines'; From d7044784f0398298ceeb98fdddf44a0a8e062308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Wed, 1 Jun 2022 14:55:10 +0200 Subject: [PATCH 24/54] Move SearchFilter from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search, and also remove SearchFilterNext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 82 ++++ plugins/search-react/package.json | 6 +- .../SearchFilter.Autocomplete.test.tsx | 361 +++++++++++++++ .../SearchFilter.Autocomplete.tsx | 120 +++++ .../SearchFilter/SearchFilter.stories.tsx | 13 +- .../SearchFilter/SearchFilter.test.tsx | 410 ++++++++++++++++++ .../components/SearchFilter/SearchFilter.tsx | 237 ++++++++++ .../components/SearchFilter/hooks.test.tsx | 8 +- .../src/components/SearchFilter/hooks.ts | 9 +- .../src/components/SearchFilter/index.ts | 24 + plugins/search-react/src/components/index.ts | 1 + plugins/search/api-report.md | 28 +- .../SearchFilter.Autocomplete.tsx | 15 +- .../components/SearchFilter/SearchFilter.tsx | 179 +------- .../src/components/SearchFilter/index.ts | 2 +- plugins/search/src/index.ts | 2 +- 16 files changed, 1296 insertions(+), 201 deletions(-) create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx rename plugins/{search => search-react}/src/components/SearchFilter/SearchFilter.stories.tsx (95%) create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.tsx rename plugins/{search => search-react}/src/components/SearchFilter/hooks.test.tsx (98%) rename plugins/{search => search-react}/src/components/SearchFilter/hooks.ts (96%) create mode 100644 plugins/search-react/src/components/SearchFilter/index.ts diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 9f61070854..00325d6f64 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -10,9 +10,18 @@ import { AsyncState } from 'react-use/lib/useAsync'; import { JsonObject } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; +import { ReactElement } from 'react'; import { SearchQuery } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; +// @public (undocumented) +export const AutocompleteFilter: ( + props: SearchAutocompleteFilterProps, +) => JSX.Element; + +// @public (undocumented) +export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; + // @public (undocumented) export const HighlightedSearchResultText: ({ text, @@ -45,6 +54,13 @@ export interface SearchApi { // @public (undocumented) export const searchApiRef: ApiRef; +// @public (undocumented) +export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { + filterSelectedOptions?: boolean; + limitTags?: number; + multiple?: boolean; +}; + // @public export const SearchContextProvider: ( props: SearchContextProviderProps, @@ -74,6 +90,72 @@ export type SearchContextValue = { fetchPreviousPage?: React_2.DispatchWithoutAction; } & SearchContextState; +// @public (undocumented) +export const SearchFilter: { + ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; + Checkbox( + props: Omit & + SearchFilterComponentProps, + ): JSX.Element; + Select( + props: Omit & + SearchFilterComponentProps, + ): JSX.Element; + Autocomplete(props: SearchAutocompleteFilterProps): JSX.Element; +}; + +// @public (undocumented) +export type SearchFilterComponentProps = { + className?: string; + name: string; + label?: string; + values?: string[] | ((partial: string) => Promise); + defaultValue?: string[] | string | null; + valuesDebounceMs?: number; +}; + +// @public (undocumented) +export type SearchFilterWrapperProps = SearchFilterComponentProps & { + component: (props: SearchFilterComponentProps) => ReactElement; + debug?: boolean; +}; + +// @public (undocumented) +export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; + +// @public +export const useAsyncFilterValues: ( + fn: ((partial: string) => Promise) | undefined, + inputValue: string, + defaultValues?: string[], + debounce?: number, +) => + | { + loading: boolean; + error?: undefined; + value?: undefined; + } + | { + loading: false; + error: Error; + value?: undefined; + } + | { + loading: true; + error?: Error | undefined; + value?: string[] | undefined; + } + | { + loading: boolean; + value: string[]; + }; + +// @public +export const useDefaultFilterValue: ( + name: string, + defaultValue?: string | string[] | null | undefined, +) => void; + // @public export const useSearch: () => SearchContextValue; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 5646ce9ee1..ac8a43dee4 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -36,7 +36,8 @@ "@backstage/version-bridge": "^1.0.1", "react-use": "^17.3.2", "@backstage/types": "^1.0.0", - "@material-ui/core": "^4.12.2" + "@material-ui/core": "^4.12.2", + "@material-ui/lab": "4.0.0-alpha.57" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", @@ -47,7 +48,8 @@ "@backstage/test-utils": "^1.1.1-next.0", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", - "@testing-library/jest-dom": "^5.10.1" + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/user-event": "^14.0.0" }, "files": [ "dist" diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx new file mode 100644 index 0000000000..14c8967b57 --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -0,0 +1,361 @@ +/* + * 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 { TestApiProvider } from '@backstage/test-utils'; +import { screen, render, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { searchApiRef } from '../../api'; +import { SearchContextProvider, useSearch } from '../../context'; +import { SearchFilter } from './SearchFilter'; + +const SearchContextFilterSpy = ({ name }: { name: string }) => { + const { filters } = useSearch(); + const value = filters[name]; + return ( + + {Array.isArray(value) ? value.join(',') : value} + + ); +}; + +describe('SearchFilter.Autocomplete', () => { + const query = jest.fn().mockResolvedValue({}); + const emptySearchContext = { + term: '', + types: [], + filters: {}, + }; + + const name = 'field'; + const values = ['value1', 'value2']; + + it('renders as expected', async () => { + render( + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + + await waitFor(() => { + screen.getByRole('listbox'); + }); + + expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument(); + }); + + it('renders as expected with async values', async () => { + render( + + + values} /> + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + + await waitFor(() => { + screen.getByRole('listbox'); + }); + + expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument(); + }); + + it('does not affect unrelated filter state', async () => { + render( + + + + + + + , + ); + + // The spy should show the initial value. + expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent( + 'value', + ); + + // Select a value from the autocomplete filter. + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[1] })); + + // Wait for the autocomplete filter's value to change. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[1], + ); + }); + + // Unrelated filter spy should maintain the same value. + expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent( + 'value', + ); + }); + + describe('single', () => { + it('renders as expected with defaultValue', async () => { + render( + + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + + await waitFor(() => { + expect(input).toHaveValue(values[1]); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[1], + ); + }); + }); + + it('renders as expected with initial context', async () => { + render( + + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + + await waitFor(() => { + expect(input).toHaveValue(values[0]); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[0], + ); + }); + }); + + it('sets filter state when selecting a value', async () => { + render( + + + + + + , + ); + + // Select the first option in the autocomplete. + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[0] })); + + // The value should be present in the context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[0], + ); + }); + + // Click the "Clear" button to remove the value. + const clearButton = within(autocomplete).getByLabelText('Clear'); + await userEvent.click(clearButton); + + // That value should have been unset from the context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(''); + }); + }); + }); + + describe('multiple', () => { + it('renders as expected with defaultValue', async () => { + render( + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText(values[0])).toBeInTheDocument(); + expect(screen.getByText(values[1])).toBeInTheDocument(); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values.join(','), + ); + }); + }); + + it('renders as expected with initial context', async () => { + render( + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText(values[0])).toBeInTheDocument(); + expect(screen.getByText(values[1])).toBeInTheDocument(); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values.join(','), + ); + }); + }); + + it('respects tag limit configuration', async () => { + render( + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + + // Select the second value. + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[1] })); + await waitFor(() => { + expect( + screen.getByRole('button', { name: values[1] }), + ).toBeInTheDocument(); + }); + + // Select the first value. + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[0] })); + await waitFor(() => { + expect( + screen.getByRole('button', { name: values[0] }), + ).toBeInTheDocument(); + }); + + // Blur the field and only one tag should be shown with a +1. + input.blur(); + expect( + screen.queryByRole('button', { name: values[0] }), + ).not.toBeInTheDocument(); + expect(screen.getByText('+1')).toBeInTheDocument(); + }); + + it('sets filter state when selecting a value', async () => { + render( + + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + + // Select both values in the autocomplete. + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[0] })); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[1] })); + + // Both options should be present in the context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values.join(','), + ); + }); + + // Click the "Clear" button to remove the value. + const clearButton = within(autocomplete).getByLabelText('Clear'); + await userEvent.click(clearButton); + + // There should be no content in the filter context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(''); + }); + }); + }); +}); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx new file mode 100644 index 0000000000..35d1f376e5 --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -0,0 +1,120 @@ +/* + * 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. + */ + +import React, { ChangeEvent, useState } from 'react'; +import { Chip, TextField } from '@material-ui/core'; +import { + Autocomplete, + AutocompleteGetTagProps, + AutocompleteRenderInputParams, +} from '@material-ui/lab'; + +import { useSearch } from '../../context'; +import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; +import { SearchFilterComponentProps } from './SearchFilter'; + +/** + * @public + */ +export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { + filterSelectedOptions?: boolean; + limitTags?: number; + multiple?: boolean; +}; + +/** + * @public + */ +export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { + const { + className, + defaultValue, + name, + values: givenValues, + valuesDebounceMs, + label, + filterSelectedOptions, + limitTags, + multiple, + } = props; + const [inputValue, setInputValue] = useState(''); + useDefaultFilterValue(name, defaultValue); + const asyncValues = + typeof givenValues === 'function' ? givenValues : undefined; + const defaultValues = + typeof givenValues === 'function' ? undefined : givenValues; + const { value: values, loading } = useAsyncFilterValues( + asyncValues, + inputValue, + defaultValues, + valuesDebounceMs, + ); + const { filters, setFilters } = useSearch(); + const filterValue = + (filters[name] as string | string[] | undefined) || (multiple ? [] : null); + + // Set new filter values on input change. + const handleChange = ( + _: ChangeEvent<{}>, + newValue: string | string[] | null, + ) => { + setFilters(prevState => { + const { [name]: filter, ...others } = prevState; + + if (newValue) { + return { ...others, [name]: newValue }; + } + return { ...others }; + }); + }; + + // Provide the input field. + const renderInput = (params: AutocompleteRenderInputParams) => ( + + ); + + // Render tags as primary-colored chips. + const renderTags = ( + tagValue: string[], + getTagProps: AutocompleteGetTagProps, + ) => + tagValue.map((option: string, index: number) => ( + + )); + + return ( + setInputValue(newValue)} + renderInput={renderInput} + renderTags={renderTags} + /> + ); +}; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx similarity index 95% rename from plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx rename to plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx index 7e8a7d585a..2c8a629f01 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -14,14 +14,13 @@ * limitations under the License. */ -import { Grid, Paper } from '@material-ui/core'; import React, { ComponentType } from 'react'; -import { - searchApiRef, - MockSearchApi, - SearchContextProvider, -} from '@backstage/plugin-search-react'; +import { Grid, Paper } from '@material-ui/core'; + import { TestApiProvider } from '@backstage/test-utils'; + +import { searchApiRef, MockSearchApi } from '../../api'; +import { SearchContextProvider } from '../../context'; import { SearchFilter } from './SearchFilter'; export default { diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx new file mode 100644 index 0000000000..89a1818e8d --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -0,0 +1,410 @@ +/* + * 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. + */ + +import React from 'react'; +import { screen, render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { SearchContextProvider } from '../../context'; +import { SearchFilter } from './SearchFilter'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn().mockReturnValue({}), +})); + +describe('SearchFilter', () => { + const initialState = { + term: '', + filters: {}, + types: [], + }; + + const label = 'Field'; + const name = 'field'; + const values = ['value1', 'value2']; + const filters = { unrelated: 'unrelated' }; + + const query = jest.fn().mockResolvedValue({}); + (useApi as jest.Mock).mockReturnValue({ query: query }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Check that element was rendered and received props', async () => { + const CustomFilter = (props: { name: string }) =>
{props.name}
; + + render(); + + expect(screen.getByRole('heading', { name })).toBeInTheDocument(); + }); + + describe('Checkbox', () => { + it('Renders field name and values when provided as props', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect( + screen.getByRole('checkbox', { name: values[0] }), + ).toBeInTheDocument(); + expect( + screen.getByRole('checkbox', { name: values[1] }), + ).toBeInTheDocument(); + }); + + it('Renders correctly based on filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect( + screen.getByRole('checkbox', { name: values[0] }), + ).not.toBeChecked(); + expect(screen.getByRole('checkbox', { name: values[1] })).toBeChecked(); + }); + + it('Renders correctly based on defaultValue', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect(screen.getByRole('checkbox', { name: values[0] })).toBeChecked(); + expect( + screen.getByRole('checkbox', { name: values[1] }), + ).not.toBeChecked(); + }); + + it('Checking / unchecking a value sets filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const checkBox = screen.getByRole('checkbox', { name: values[0] }); + + // Check the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters: { field: [values[0]] } }), + ); + }); + + // Uncheck the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters: {} }), + ); + }); + }); + + it('Checking / unchecking a value maintains unrelated filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const checkBox = screen.getByRole('checkbox', { name: values[0] }); + + // Check the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { ...filters, field: [values[0]] }, + }), + ); + }); + + // Uncheck the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters }), + ); + }); + }); + }); + + describe('Select', () => { + it('Renders field name and values when provided as props', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('option', { name: values[0] }), + ).toBeInTheDocument(); + expect( + screen.getByRole('option', { name: values[1] }), + ).toBeInTheDocument(); + }); + + it('Renders values when provided asynchronously', async () => { + render( + + values} + /> + , + ); + + await waitFor(() => { + expect(screen.getByRole('button')).toBeInTheDocument(); + expect( + screen.getByRole('button').getAttribute('aria-disabled'), + ).not.toBe('true'); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('option', { name: values[0] }), + ).toBeInTheDocument(); + expect( + screen.getByRole('option', { name: values[1] }), + ).toBeInTheDocument(); + }); + + it('Renders correctly based on filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect(screen.getByRole('option', { name: values[0] })).toHaveAttribute( + 'aria-selected', + 'true', + ); + expect( + screen.getByRole('option', { name: values[1] }), + ).not.toHaveAttribute('aria-selected'); + expect(screen.getByRole('option', { name: 'All' })).not.toHaveAttribute( + 'aria-selected', + ); + }); + + it('Renders correctly based on defaultValue', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect(screen.getByRole('option', { name: values[0] })).toHaveAttribute( + 'aria-selected', + 'true', + ); + expect( + screen.getByRole('option', { name: values[1] }), + ).not.toHaveAttribute('aria-selected'); + expect(screen.getByRole('option', { name: 'All' })).not.toHaveAttribute( + 'aria-selected', + ); + }); + + it('Selecting a value sets filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const button = screen.getByRole('button'); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: values[0] })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { [name]: values[0] }, + }), + ); + }); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: 'All' })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: {}, + }), + ); + }); + }); + + it('Selecting a value maintains unrelated filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const button = screen.getByRole('button'); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: values[0] })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { ...filters, [name]: values[0] }, + }), + ); + }); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: 'All' })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters }), + ); + }); + }); + }); +}); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx new file mode 100644 index 0000000000..2a6d7baafa --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx @@ -0,0 +1,237 @@ +/* + * 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. + */ + +import React, { ReactElement, ChangeEvent } from 'react'; +import { + makeStyles, + FormControl, + FormControlLabel, + InputLabel, + Checkbox, + Select, + MenuItem, + FormLabel, +} from '@material-ui/core'; + +import { useSearch } from '../../context'; +import { + AutocompleteFilter, + SearchAutocompleteFilterProps, +} from './SearchFilter.Autocomplete'; +import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; + +const useStyles = makeStyles({ + label: { + textTransform: 'capitalize', + }, +}); + +/** + * @public + */ +export type SearchFilterComponentProps = { + className?: string; + name: string; + label?: string; + /** + * Either an array of values directly, or an async function to return a list + * of values to be used in the filter. In the autocomplete filter, the last + * input value is provided as an input to allow values to be filtered. This + * function is debounced and values cached. + */ + values?: string[] | ((partial: string) => Promise); + defaultValue?: string[] | string | null; + /** + * Debounce time in milliseconds, used when values is an async callback. + * Defaults to 250ms. + */ + valuesDebounceMs?: number; +}; + +/** + * @public + */ +export type SearchFilterWrapperProps = SearchFilterComponentProps & { + component: (props: SearchFilterComponentProps) => ReactElement; + debug?: boolean; +}; + +/** + * @public + */ +export const CheckboxFilter = (props: SearchFilterComponentProps) => { + const { + className, + defaultValue, + label, + name, + values: givenValues = [], + valuesDebounceMs, + } = props; + const classes = useStyles(); + const { filters, setFilters } = useSearch(); + useDefaultFilterValue(name, defaultValue); + const asyncValues = + typeof givenValues === 'function' ? givenValues : undefined; + const defaultValues = + typeof givenValues === 'function' ? undefined : givenValues; + const { value: values = [], loading } = useAsyncFilterValues( + asyncValues, + '', + defaultValues, + valuesDebounceMs, + ); + + const handleChange = (e: ChangeEvent) => { + const { + target: { value, checked }, + } = e; + + setFilters(prevFilters => { + const { [name]: filter, ...others } = prevFilters; + const rest = ((filter as string[]) || []).filter(i => i !== value); + const items = checked ? [...rest, value] : rest; + return items.length ? { ...others, [name]: items } : others; + }); + }; + + return ( + + {label ? {label} : null} + {values.map((value: string) => ( + + } + label={value} + /> + ))} + + ); +}; + +/** + * @public + */ +export const SelectFilter = (props: SearchFilterComponentProps) => { + const { + className, + defaultValue, + label, + name, + values: givenValues, + valuesDebounceMs, + } = props; + const classes = useStyles(); + useDefaultFilterValue(name, defaultValue); + const asyncValues = + typeof givenValues === 'function' ? givenValues : undefined; + const defaultValues = + typeof givenValues === 'function' ? undefined : givenValues; + const { value: values = [], loading } = useAsyncFilterValues( + asyncValues, + '', + defaultValues, + valuesDebounceMs, + ); + const { filters, setFilters } = useSearch(); + + const handleChange = (e: ChangeEvent<{ value: unknown }>) => { + const { + target: { value }, + } = e; + + setFilters(prevFilters => { + const { [name]: filter, ...others } = prevFilters; + return value ? { ...others, [name]: value as string } : others; + }); + }; + + return ( + + {label ? ( + + {label} + + ) : null} + + + ); +}; + +/** + * @public + */ +const SearchFilter = ({ + component: Element, + ...props +}: SearchFilterWrapperProps) => ; + +SearchFilter.Checkbox = ( + props: Omit & + SearchFilterComponentProps, +) => ; + +SearchFilter.Select = ( + props: Omit & + SearchFilterComponentProps, +) => ; + +/** + * A control surface for a given filter field name, rendered as an autocomplete + * textfield. A hard-coded list of values may be provided, or an async function + * which returns values may be provided instead. + * + * @public + */ +SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( + +); + +export { SearchFilter }; diff --git a/plugins/search/src/components/SearchFilter/hooks.test.tsx b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx similarity index 98% rename from plugins/search/src/components/SearchFilter/hooks.test.tsx rename to plugins/search-react/src/components/SearchFilter/hooks.test.tsx index 14346a29eb..80c4a3a3a2 100644 --- a/plugins/search/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx @@ -17,11 +17,9 @@ import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; -import { - SearchContextProvider, - useSearch, - searchApiRef, -} from '@backstage/plugin-search-react'; + +import { searchApiRef } from '../../api'; +import { SearchContextProvider, useSearch } from '../../context'; import { useDefaultFilterValue, useAsyncFilterValues } from './hooks'; jest.useFakeTimers(); diff --git a/plugins/search/src/components/SearchFilter/hooks.ts b/plugins/search-react/src/components/SearchFilter/hooks.ts similarity index 96% rename from plugins/search/src/components/SearchFilter/hooks.ts rename to plugins/search-react/src/components/SearchFilter/hooks.ts index da30466522..5771cc5288 100644 --- a/plugins/search/src/components/SearchFilter/hooks.ts +++ b/plugins/search-react/src/components/SearchFilter/hooks.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -17,11 +17,14 @@ import { useEffect, useRef } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useDebounce from 'react-use/lib/useDebounce'; -import { useSearch } from '@backstage/plugin-search-react'; + +import { useSearch } from '../../context'; /** * Utility hook for either asynchronously loading filter values from a given * function or synchronously providing a given list of default values. + * + * @public */ export const useAsyncFilterValues = ( fn: ((partial: string) => Promise) | undefined, @@ -75,6 +78,8 @@ export const useAsyncFilterValues = ( /** * Utility hook for applying a given default value to the search context. + * + * @public */ export const useDefaultFilterValue = ( name: string, diff --git a/plugins/search-react/src/components/SearchFilter/index.ts b/plugins/search-react/src/components/SearchFilter/index.ts new file mode 100644 index 0000000000..e159cff93d --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export { CheckboxFilter, SearchFilter, SelectFilter } from './SearchFilter'; +export { AutocompleteFilter } from './SearchFilter.Autocomplete'; +export { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; +export type { + SearchFilterComponentProps, + SearchFilterWrapperProps, +} from './SearchFilter'; +export type { SearchAutocompleteFilterProps } from './SearchFilter.Autocomplete'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 679a254827..8e45c66fce 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -15,3 +15,4 @@ */ export * from './HighlightedSearchResultText'; +export * from './SearchFilter'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 21b9df5dda..ec60569af4 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -12,7 +12,9 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react'; import { SearchDocument } from '@backstage/plugin-search-common'; +import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -78,8 +80,8 @@ export type HomePageSearchBarProps = Partial< // @public (undocumented) export const Router: () => JSX.Element; -// @public (undocumented) -export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { +// @public @deprecated (undocumented) +export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & { filterSelectedOptions?: boolean; limitTags?: number; multiple?: boolean; @@ -124,7 +126,7 @@ export type SearchBarProps = Partial; // Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const SearchFilter: { ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; Checkbox( @@ -135,10 +137,10 @@ export const SearchFilter: { props: Omit & SearchFilterComponentProps, ): JSX.Element; - Autocomplete(props: SearchAutocompleteFilterProps): JSX.Element; + Autocomplete(props: SearchAutocompleteFilterProps_2): JSX.Element; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type SearchFilterComponentProps = { className?: string; name: string; @@ -148,23 +150,7 @@ export type SearchFilterComponentProps = { valuesDebounceMs?: number; }; -// Warning: (ae-missing-release-tag) "SearchFilterNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) -export const SearchFilterNext: { - ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; - Checkbox( - props: Omit & - SearchFilterComponentProps, - ): JSX.Element; - Select( - props: Omit & - SearchFilterComponentProps, - ): JSX.Element; - Autocomplete(props: SearchAutocompleteFilterProps): JSX.Element; -}; - -// @public (undocumented) export type SearchFilterWrapperProps = SearchFilterComponentProps & { component: (props: SearchFilterComponentProps) => ReactElement; debug?: boolean; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index 03f4f28943..1a74decd31 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -21,11 +21,16 @@ import { AutocompleteGetTagProps, AutocompleteRenderInputParams, } from '@material-ui/lab'; -import { useSearch } from '@backstage/plugin-search-react'; -import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; -import { SearchFilterComponentProps } from './SearchFilter'; +import { + SearchFilterComponentProps, + useSearch, + useAsyncFilterValues, + useDefaultFilterValue, +} from '@backstage/plugin-search-react'; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * @public */ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { @@ -34,6 +39,10 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { multiple?: boolean; }; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + * + */ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { const { className, diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index bfbab8531c..b88160f363 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -14,32 +14,18 @@ * limitations under the License. */ -import React, { ReactElement, ChangeEvent } from 'react'; -import { - makeStyles, - FormControl, - FormControlLabel, - InputLabel, - Checkbox, - Select, - MenuItem, - FormLabel, -} from '@material-ui/core'; +import React, { ReactElement } from 'react'; import { AutocompleteFilter, + CheckboxFilter, SearchAutocompleteFilterProps, -} from './SearchFilter.Autocomplete'; -import { useSearch } from '@backstage/plugin-search-react'; -import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; - -const useStyles = makeStyles({ - label: { - textTransform: 'capitalize', - }, -}); + SelectFilter, +} from '@backstage/plugin-search-react'; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * @public */ export type SearchFilterComponentProps = { @@ -62,6 +48,8 @@ export type SearchFilterComponentProps = { }; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * @public */ export type SearchFilterWrapperProps = SearchFilterComponentProps & { @@ -69,152 +57,33 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -const CheckboxFilter = (props: SearchFilterComponentProps) => { - const { - className, - defaultValue, - label, - name, - values: givenValues = [], - valuesDebounceMs, - } = props; - const classes = useStyles(); - const { filters, setFilters } = useSearch(); - useDefaultFilterValue(name, defaultValue); - const asyncValues = - typeof givenValues === 'function' ? givenValues : undefined; - const defaultValues = - typeof givenValues === 'function' ? undefined : givenValues; - const { value: values = [], loading } = useAsyncFilterValues( - asyncValues, - '', - defaultValues, - valuesDebounceMs, - ); - - const handleChange = (e: ChangeEvent) => { - const { - target: { value, checked }, - } = e; - - setFilters(prevFilters => { - const { [name]: filter, ...others } = prevFilters; - const rest = ((filter as string[]) || []).filter(i => i !== value); - const items = checked ? [...rest, value] : rest; - return items.length ? { ...others, [name]: items } : others; - }); - }; - - return ( - - {label ? {label} : null} - {values.map((value: string) => ( - - } - label={value} - /> - ))} - - ); -}; - -const SelectFilter = (props: SearchFilterComponentProps) => { - const { - className, - defaultValue, - label, - name, - values: givenValues, - valuesDebounceMs, - } = props; - const classes = useStyles(); - useDefaultFilterValue(name, defaultValue); - const asyncValues = - typeof givenValues === 'function' ? givenValues : undefined; - const defaultValues = - typeof givenValues === 'function' ? undefined : givenValues; - const { value: values = [], loading } = useAsyncFilterValues( - asyncValues, - '', - defaultValues, - valuesDebounceMs, - ); - const { filters, setFilters } = useSearch(); - - const handleChange = (e: ChangeEvent<{ value: unknown }>) => { - const { - target: { value }, - } = e; - - setFilters(prevFilters => { - const { [name]: filter, ...others } = prevFilters; - return value ? { ...others, [name]: value as string } : others; - }); - }; - - return ( - - {label ? ( - - {label} - - ) : null} - - - ); -}; - +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ const SearchFilter = ({ component: Element, ...props }: SearchFilterWrapperProps) => ; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ SearchFilter.Checkbox = ( props: Omit & SearchFilterComponentProps, ) => ; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ SearchFilter.Select = ( props: Omit & SearchFilterComponentProps, ) => ; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * A control surface for a given filter field name, rendered as an autocomplete * textfield. A hard-coded list of values may be provided, or an async function * which returns values may be provided instead. @@ -224,12 +93,4 @@ SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -const SearchFilterNext = SearchFilter; - -export { SearchFilter, SearchFilterNext }; +export { SearchFilter }; diff --git a/plugins/search/src/components/SearchFilter/index.ts b/plugins/search/src/components/SearchFilter/index.ts index 86cd66ff6b..3050330038 100644 --- a/plugins/search/src/components/SearchFilter/index.ts +++ b/plugins/search/src/components/SearchFilter/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { SearchFilter, SearchFilterNext } from './SearchFilter'; +export { SearchFilter } from './SearchFilter'; export type { SearchFilterComponentProps, SearchFilterWrapperProps, diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 1457ac781d..2eb2ed277b 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -28,7 +28,7 @@ export type { SearchBarBaseProps, SearchBarProps, } from './components/SearchBar'; -export { SearchFilter, SearchFilterNext } from './components/SearchFilter'; +export { SearchFilter } from './components/SearchFilter'; export type { SearchAutocompleteFilterProps, SearchFilterComponentProps, From 01abb3bc7e4ee0a7d1b806fc63662a1ad6106688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Wed, 1 Jun 2022 16:37:19 +0200 Subject: [PATCH 25/54] Move SearchResult from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 9 ++ plugins/search-react/package.json | 7 +- .../SearchResult/SearchResult.stories.tsx | 11 +- .../SearchResult/SearchResult.test.tsx | 125 ++++++++++++++++++ .../components/SearchResult/SearchResult.tsx | 60 +++++++++ .../src/components/SearchResult/index.tsx | 18 +++ plugins/search-react/src/components/index.ts | 1 + .../components/SearchResult/SearchResult.tsx | 3 + 8 files changed, 225 insertions(+), 9 deletions(-) rename plugins/{search => search-react}/src/components/SearchResult/SearchResult.stories.tsx (92%) create mode 100644 plugins/search-react/src/components/SearchResult/SearchResult.test.tsx create mode 100644 plugins/search-react/src/components/SearchResult/SearchResult.tsx create mode 100644 plugins/search-react/src/components/SearchResult/index.tsx diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 00325d6f64..09dd3d77a8 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -12,6 +12,7 @@ import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; // @public (undocumented) @@ -120,6 +121,14 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; +// @public (undocumented) +export const SearchResult: ({ children }: SearchResultProps) => JSX.Element; + +// @public (undocumented) +export type SearchResultProps = { + children: (results: { results: SearchResult_2[] }) => JSX.Element; +}; + // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index ac8a43dee4..04932bd5ea 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -31,13 +31,16 @@ "start": "backstage-cli package start" }, "dependencies": { + "@backstage/plugin-search": "0.8.2-next.1", "@backstage/plugin-search-common": "^0.3.5-next.0", + "@backstage/core-components": "^0.9.5-next.1", "@backstage/core-plugin-api": "^1.0.3-next.0", "@backstage/version-bridge": "^1.0.1", - "react-use": "^17.3.2", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", - "@material-ui/lab": "4.0.0-alpha.57" + "@material-ui/lab": "4.0.0-alpha.57", + "react-router": "6.0.0-beta.0", + "react-use": "^17.3.2" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx similarity index 92% rename from plugins/search/src/components/SearchResult/SearchResult.stories.tsx rename to plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index 207f1ae1d7..38be586f97 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -18,13 +18,10 @@ import { Link } from '@backstage/core-components'; import { List, ListItem } from '@material-ui/core'; import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router'; -import { DefaultResultListItem } from '../DefaultResultListItem'; +import { DefaultResultListItem } from '@backstage/plugin-search'; -import { - searchApiRef, - MockSearchApi, - SearchContextProvider, -} from '@backstage/plugin-search-react'; +import { searchApiRef, MockSearchApi } from '../../api'; +import { SearchContextProvider } from '../../context'; import { SearchResult } from './SearchResult'; import { TestApiProvider } from '@backstage/test-utils'; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx new file mode 100644 index 0000000000..10437b3af7 --- /dev/null +++ b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx @@ -0,0 +1,125 @@ +/* + * 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. + */ + +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { useSearch } from '../../context'; +import { SearchResult } from './SearchResult'; + +jest.mock('../../context', () => ({ + ...jest.requireActual('../../context'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchResult', () => { + it('Progress rendered on Loading state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: true }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect(getByRole('progressbar')).toBeInTheDocument(); + }); + }); + + it('Alert rendered on Error state', async () => { + const error = new Error('some error'); + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + new RegExp(`Error encountered while fetching search results.*${error}`), + ); + }); + }); + + it('On no result value state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error: '', value: undefined }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect( + getByRole('heading', { name: 'Sorry, no results were found' }), + ).toBeInTheDocument(); + }); + }); + + it('On empty result value state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error: '', value: { results: [] } }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect( + getByRole('heading', { name: 'Sorry, no results were found' }), + ).toBeInTheDocument(); + }); + }); + + it('Calls children with results set to result.value', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { + loading: false, + error: '', + value: { + totalCount: 1, + results: [ + { + type: 'some-type', + document: { + title: 'some-title', + text: 'some-text', + location: 'some-location', + }, + }, + ], + }, + }, + }); + + const { getByText } = await renderInTestApp( + + {({ results }) => { + return <>Results {results.length}; + }} + , + ); + + expect(getByText('Results 1')).toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx new file mode 100644 index 0000000000..a1ce3cea25 --- /dev/null +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -0,0 +1,60 @@ +/* + * 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. + */ + +import { + EmptyState, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { SearchResult } from '@backstage/plugin-search-common'; +import React from 'react'; +import { useSearch } from '../../context'; + +/** + * @public + */ +export type SearchResultProps = { + children: (results: { results: SearchResult[] }) => JSX.Element; +}; + +/** + * @public + */ +export const SearchResultComponent = ({ children }: SearchResultProps) => { + const { + result: { loading, error, value }, + } = useSearch(); + + if (loading) { + return ; + } + if (error) { + return ( + + ); + } + + if (!value?.results.length) { + return ; + } + + return <>{children({ results: value.results })}; +}; + +export { SearchResultComponent as SearchResult }; diff --git a/plugins/search-react/src/components/SearchResult/index.tsx b/plugins/search-react/src/components/SearchResult/index.tsx new file mode 100644 index 0000000000..2032fbbed9 --- /dev/null +++ b/plugins/search-react/src/components/SearchResult/index.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { SearchResult } from './SearchResult'; +export type { SearchResultProps } from './SearchResult'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 8e45c66fce..db9214d299 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -16,3 +16,4 @@ export * from './HighlightedSearchResultText'; export * from './SearchFilter'; +export * from './SearchResult'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index a62dddb924..77ee883164 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -27,6 +27,9 @@ type Props = { children: (results: { results: SearchResult[] }) => JSX.Element; }; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ export const SearchResultComponent = ({ children }: Props) => { const { result: { loading, error, value }, From 193ae37f15760d95ea2e45bdd0c830e4551aac61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Wed, 1 Jun 2022 16:50:03 +0200 Subject: [PATCH 26/54] Move SearchResultPager from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 3 + plugins/search-react/package.json | 1 + .../SearchResultPager.test.tsx | 58 ++++++++++++++++ .../SearchResultPager/SearchResultPager.tsx | 66 +++++++++++++++++++ .../src/components/SearchResultPager/index.ts | 17 +++++ plugins/search-react/src/components/index.ts | 1 + plugins/search/api-report.md | 2 +- .../SearchResultPager/SearchResultPager.tsx | 3 + 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx create mode 100644 plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx create mode 100644 plugins/search-react/src/components/SearchResultPager/index.ts diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 09dd3d77a8..5558fd3257 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -124,6 +124,9 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { // @public (undocumented) export const SearchResult: ({ children }: SearchResultProps) => JSX.Element; +// @public (undocumented) +export const SearchResultPager: () => JSX.Element; + // @public (undocumented) export type SearchResultProps = { children: (results: { results: SearchResult_2[] }) => JSX.Element; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 04932bd5ea..95507e64b7 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -38,6 +38,7 @@ "@backstage/version-bridge": "^1.0.1", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-router": "6.0.0-beta.0", "react-use": "^17.3.2" diff --git a/plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx new file mode 100644 index 0000000000..24d9a76e51 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx @@ -0,0 +1,58 @@ +/* + * 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. + */ + +import React from 'react'; + +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { useSearch } from '../../context'; +import { SearchResultPager } from './SearchResultPager'; + +jest.mock('../../context', () => ({ + ...jest.requireActual('../../context'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchResultPager', () => { + it('renders pager buttons', async () => { + const fetchNextPage = jest.fn(); + const fetchPreviousPage = jest.fn(); + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, value: [] }, + fetchNextPage, + fetchPreviousPage, + }); + + const { getByLabelText } = await renderInTestApp(); + + await waitFor(() => { + expect(getByLabelText('previous page')).toBeInTheDocument(); + }); + await userEvent.click(getByLabelText('previous page')); + expect(fetchPreviousPage).toBeCalled(); + + await waitFor(() => { + expect(getByLabelText('next page')).toBeInTheDocument(); + }); + await userEvent.click(getByLabelText('next page')); + + expect(fetchNextPage).toBeCalled(); + }); +}); diff --git a/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx new file mode 100644 index 0000000000..68f8223ca0 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx @@ -0,0 +1,66 @@ +/* + * 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. + */ + +import React from 'react'; + +import { Button, makeStyles } from '@material-ui/core'; +import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; + +import { useSearch } from '../../context'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + justifyContent: 'space-between', + gap: theme.spacing(2), + margin: theme.spacing(2, 0), + }, +})); + +/** + * @public + */ +export const SearchResultPager = () => { + const { fetchNextPage, fetchPreviousPage } = useSearch(); + const classes = useStyles(); + + if (!fetchNextPage && !fetchPreviousPage) { + return <>; + } + + return ( + + ); +}; diff --git a/plugins/search-react/src/components/SearchResultPager/index.ts b/plugins/search-react/src/components/SearchResultPager/index.ts new file mode 100644 index 0000000000..5ff203b15d --- /dev/null +++ b/plugins/search-react/src/components/SearchResultPager/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { SearchResultPager } from './SearchResultPager'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index db9214d299..bfd65d471f 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -17,3 +17,4 @@ export * from './HighlightedSearchResultText'; export * from './SearchFilter'; export * from './SearchResult'; +export * from './SearchResultPager'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index ec60569af4..25a7f40394 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -237,7 +237,7 @@ export const SearchResult: ({ // Warning: (ae-missing-release-tag) "SearchResultPager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const SearchResultPager: () => JSX.Element; // Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index 6f4c8628a6..0e8d59a694 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -29,6 +29,9 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ export const SearchResultPager = () => { const { fetchNextPage, fetchPreviousPage } = useSearch(); const classes = useStyles(); From ccbabfd4bdaea8319a911d2c4f718a8af153473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Thu, 2 Jun 2022 10:49:09 +0200 Subject: [PATCH 27/54] Move SearchBar, SearchBarBase, and SearchTracker from @backstage/plugin-search to @backstage/plugin-search-react and deprecate SearchBar and SearchbarBase in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 37 ++ .../SearchBar/SearchBar.stories.tsx | 14 +- .../components/SearchBar/SearchBar.test.tsx | 352 ++++++++++++++++++ .../src/components/SearchBar/SearchBar.tsx | 179 +++++++++ .../src/components/SearchBar/index.tsx | 18 + .../SearchTracker/SearchTracker.tsx | 6 +- .../src/components/SearchTracker/index.ts | 2 +- plugins/search-react/src/components/index.ts | 2 + plugins/search/api-report.md | 8 +- .../src/components/SearchBar/SearchBar.tsx | 10 +- 10 files changed, 613 insertions(+), 15 deletions(-) rename plugins/{search => search-react}/src/components/SearchBar/SearchBar.stories.tsx (93%) create mode 100644 plugins/search-react/src/components/SearchBar/SearchBar.test.tsx create mode 100644 plugins/search-react/src/components/SearchBar/SearchBar.tsx create mode 100644 plugins/search-react/src/components/SearchBar/index.tsx rename plugins/{search => search-react}/src/components/SearchTracker/SearchTracker.tsx (91%) rename plugins/{search => search-react}/src/components/SearchTracker/index.ts (93%) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 5558fd3257..27279e09a9 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; +import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -62,6 +63,35 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { multiple?: boolean; }; +// @public +export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; + +// @public +export const SearchBarBase: ({ + onChange, + onKeyDown, + onSubmit, + debounceTime, + clearButton, + fullWidth, + value: defaultValue, + inputProps: defaultInputProps, + endAdornment: defaultEndAdornment, + ...props +}: SearchBarBaseProps) => JSX.Element; + +// @public +export type SearchBarBaseProps = Omit & { + debounceTime?: number; + clearButton?: boolean; + onClear?: () => void; + onSubmit?: () => void; + onChange: (value: string) => void; +}; + +// @public +export type SearchBarProps = Partial; + // @public export const SearchContextProvider: ( props: SearchContextProviderProps, @@ -135,6 +165,13 @@ export type SearchResultProps = { // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; +// @public +export const TrackSearch: ({ + children, +}: { + children: React_2.ReactChild; +}) => JSX.Element; + // @public export const useAsyncFilterValues: ( fn: ((partial: string) => Promise) | undefined, diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx similarity index 93% rename from plugins/search/src/components/SearchBar/SearchBar.stories.tsx rename to plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx index 9728101e97..88aed35f8d 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Grid, makeStyles, Paper } from '@material-ui/core'; import React, { ComponentType } from 'react'; -import { - searchApiRef, - MockSearchApi, - SearchContextProvider, -} from '@backstage/plugin-search-react'; +import { Grid, makeStyles, Paper } from '@material-ui/core'; + import { TestApiProvider } from '@backstage/test-utils'; + +import { searchApiRef, MockSearchApi } from '../../api'; +import { SearchContextProvider } from '../../context'; + import { SearchBar } from './SearchBar'; export default { diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx new file mode 100644 index 0000000000..4e2c4d7a5c --- /dev/null +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -0,0 +1,352 @@ +/* + * 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. + */ + +import React from 'react'; +import { screen, render, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils'; + +import { searchApiRef } from '../../api'; +import { SearchContextProvider } from '../../context'; +import { SearchBar } from './SearchBar'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), +})); + +describe('SearchBar', () => { + const initialState = { + term: '', + filters: {}, + types: ['*'], + pageCursor: '', + }; + + const query = jest.fn().mockResolvedValue({}); + const analyticsApiSpy = new MockAnalyticsApi(); + let apiRegistry: TestApiRegistry; + + apiRegistry = TestApiRegistry.from( + [ + configApiRef, + new ConfigReader({ + app: { title: 'Mock title' }, + }), + ], + [searchApiRef, { query }], + ); + + const name = 'Search'; + const term = 'term'; + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Renders without exploding', async () => { + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Search in Mock title'), + ).toBeInTheDocument(); + }); + }); + + it('Renders with custom placeholder', async () => { + render( + + + + + , + , + ); + + await waitFor(() => { + expect( + screen.getByPlaceholderText('This is a custom placeholder'), + ).toBeInTheDocument(); + }); + }); + + it('Renders based on initial search', async () => { + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(term); + }); + }); + + it('Updates term state when text is entered', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + const defaultDebounceTime = 200; + + render( + + + + + , + , + ); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + act(() => { + jest.advanceTimersByTime(defaultDebounceTime); + }); + + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + jest.useRealTimers(); + }); + + it('Clear button clears term state', async () => { + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(term); + }); + + await userEvent.click(screen.getByRole('button', { name: 'Clear' })); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(''); + }); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ term: '' }), + ); + }); + + it('Should not show clear button', async () => { + render( + + + + + , + ); + + await waitFor(() => { + expect( + screen.queryByRole('button', { name: 'Clear' }), + ).not.toBeInTheDocument(); + }); + }); + + it('Adheres to provided debounceTime', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + + const debounceTime = 600; + + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + expect(query).not.toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + expect(textbox).toHaveValue(value); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + jest.useRealTimers(); + }); + + it('does not capture analytics event if not enabled in app', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + + const debounceTime = 600; + + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue(value)); + + expect(analyticsApiSpy.getEvents()).toHaveLength(0); + jest.useRealTimers(); + }); + + it('captures analytics events if enabled in app', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + + const debounceTime = 600; + + apiRegistry = TestApiRegistry.from( + [analyticsApiRef, analyticsApiSpy], + [ + configApiRef, + new ConfigReader({ + app: { + title: 'Mock title', + analytics: { + ga: { + trackingId: 'xyz123', + }, + }, + }, + }), + ], + [searchApiRef, { query }], + ); + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + expect(analyticsApiSpy.getEvents()).toHaveLength(0); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue(value)); + + expect(analyticsApiSpy.getEvents()).toHaveLength(1); + expect(analyticsApiSpy.getEvents()[0]).toEqual({ + action: 'search', + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + searchTypes: 'software-catalog,techdocs', + }, + subject: 'value', + }); + + await user.clear(textbox); + + // make sure new term is captured + await user.type(textbox, 'new value'); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue('new value')); + + expect(analyticsApiSpy.getEvents()).toHaveLength(2); + expect(analyticsApiSpy.getEvents()[1]).toEqual({ + action: 'search', + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + searchTypes: 'software-catalog,techdocs', + }, + subject: 'new value', + }); + jest.useRealTimers(); + }); +}); diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 0000000000..b3c8ec9fa5 --- /dev/null +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -0,0 +1,179 @@ +/* + * 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. + */ + +import React, { + ChangeEvent, + KeyboardEvent, + useState, + useEffect, + useCallback, +} from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + InputBase, + InputBaseProps, + InputAdornment, + IconButton, +} from '@material-ui/core'; +import SearchIcon from '@material-ui/icons/Search'; +import ClearButton from '@material-ui/icons/Clear'; + +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +import { + SearchContextProvider, + useSearch, + useSearchContextCheck, +} from '../../context'; +import { TrackSearch } from '../SearchTracker'; + +/** + * Props for {@link SearchBarBase}. + * + * @public + */ +export type SearchBarBaseProps = Omit & { + debounceTime?: number; + clearButton?: boolean; + onClear?: () => void; + onSubmit?: () => void; + onChange: (value: string) => void; +}; + +/** + * All search boxes exported by the search plugin are based on the , + * and this one is based on the component from Material UI. + * Recommended if you don't use Search Provider or Search Context. + * + * @public + */ +export const SearchBarBase = ({ + onChange, + onKeyDown, + onSubmit, + debounceTime = 200, + clearButton = true, + fullWidth = true, + value: defaultValue, + inputProps: defaultInputProps = {}, + endAdornment: defaultEndAdornment, + ...props +}: SearchBarBaseProps) => { + const configApi = useApi(configApiRef); + const [value, setValue] = useState(defaultValue as string); + const hasSearchContext = useSearchContextCheck(); + + useEffect(() => { + setValue(prevValue => + prevValue !== defaultValue ? (defaultValue as string) : prevValue, + ); + }, [defaultValue]); + + useDebounce(() => onChange(value), debounceTime, [value]); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setValue(e.target.value); + }, + [setValue], + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (onKeyDown) onKeyDown(e); + if (onSubmit && e.key === 'Enter') { + onSubmit(); + } + }, + [onKeyDown, onSubmit], + ); + + const handleClear = useCallback(() => { + onChange(''); + }, [onChange]); + + const placeholder = `Search in ${ + configApi.getOptionalString('app.title') || 'Backstage' + }`; + + const startAdornment = ( + + + + + + ); + + const endAdornment = ( + + + + + + ); + + const searchBar = ( + + + + ); + + return hasSearchContext ? ( + searchBar + ) : ( + {searchBar} + ); +}; + +/** + * Props for {@link SearchBar}. + * + * @public + */ +export type SearchBarProps = Partial; + +/** + * Recommended search bar when you use the Search Provider or Search Context. + * + * @public + */ +export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { + const { term, setTerm } = useSearch(); + + const handleChange = useCallback( + (newValue: string) => { + if (onChange) { + onChange(newValue); + } else { + setTerm(newValue); + } + }, + [onChange, setTerm], + ); + + return ; +}; diff --git a/plugins/search-react/src/components/SearchBar/index.tsx b/plugins/search-react/src/components/SearchBar/index.tsx new file mode 100644 index 0000000000..075a0c7dc2 --- /dev/null +++ b/plugins/search-react/src/components/SearchBar/index.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { SearchBar, SearchBarBase } from './SearchBar'; +export type { SearchBarProps, SearchBarBaseProps } from './SearchBar'; diff --git a/plugins/search/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx similarity index 91% rename from plugins/search/src/components/SearchTracker/SearchTracker.tsx rename to plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index 35cfa1ec38..8ca7373298 100644 --- a/plugins/search/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -16,10 +16,12 @@ import React, { useEffect } from 'react'; import { useAnalytics } from '@backstage/core-plugin-api'; -import { useSearch } from '@backstage/plugin-search-react'; +import { useSearch } from '../../context'; /** * Capture search event on term change. + * + * @public */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { const analytics = useAnalytics(); diff --git a/plugins/search/src/components/SearchTracker/index.ts b/plugins/search-react/src/components/SearchTracker/index.ts similarity index 93% rename from plugins/search/src/components/SearchTracker/index.ts rename to plugins/search-react/src/components/SearchTracker/index.ts index 5e6a75aa25..9932f2eaec 100644 --- a/plugins/search/src/components/SearchTracker/index.ts +++ b/plugins/search-react/src/components/SearchTracker/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index bfd65d471f..5b7767859e 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -18,3 +18,5 @@ export * from './HighlightedSearchResultText'; export * from './SearchFilter'; export * from './SearchResult'; export * from './SearchResultPager'; +export * from './SearchBar'; +export * from './SearchTracker'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 25a7f40394..ea5f0b4d0d 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -87,10 +87,10 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & { multiple?: boolean; }; -// @public +// @public @deprecated (undocumented) export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; -// @public +// @public @deprecated (undocumented) export const SearchBarBase: ({ onChange, onKeyDown, @@ -104,7 +104,7 @@ export const SearchBarBase: ({ ...props }: SearchBarBaseProps) => JSX.Element; -// @public +// @public @deprecated (undocumented) export type SearchBarBaseProps = Omit & { debounceTime?: number; clearButton?: boolean; @@ -121,7 +121,7 @@ export const SearchBarNext: ({ ...props }: Partial) => JSX.Element; -// @public +// @public @deprecated (undocumented) export type SearchBarProps = Partial; // Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index b5d55fed8e..cb61089eb6 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -36,10 +36,12 @@ import { SearchContextProvider, useSearch, useSearchContextCheck, + TrackSearch, } from '@backstage/plugin-search-react'; -import { TrackSearch } from '../SearchTracker'; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * Props for {@link SearchBarBase}. * * @public @@ -53,6 +55,8 @@ export type SearchBarBaseProps = Omit & { }; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * All search boxes exported by the search plugin are based on the , * and this one is based on the component from Material UI. * Recommended if you don't use Search Provider or Search Context. @@ -149,6 +153,8 @@ export const SearchBarBase = ({ }; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * Props for {@link SearchBar}. * * @public @@ -156,6 +162,8 @@ export const SearchBarBase = ({ export type SearchBarProps = Partial; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * Recommended search bar when you use the Search Provider or Search Context. * * @public From b3389f1eeef23a8d025370b849f63b0be6856963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Thu, 2 Jun 2022 13:15:29 +0200 Subject: [PATCH 28/54] remove Search*Next components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search/api-report.md | 14 ----------- plugins/search/src/index.ts | 2 -- plugins/search/src/plugin.ts | 49 ------------------------------------ 3 files changed, 65 deletions(-) diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index ea5f0b4d0d..0527c46fe8 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -113,14 +113,6 @@ export type SearchBarBaseProps = Omit & { onChange: (value: string) => void; }; -// Warning: (ae-missing-release-tag) "SearchBarNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const SearchBarNext: ({ - onChange, - ...props -}: Partial) => JSX.Element; - // @public @deprecated (undocumented) export type SearchBarProps = Partial; @@ -208,18 +200,12 @@ export type SearchModalValue = { // @public (undocumented) export const SearchPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchPageNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const SearchPageNext: () => JSX.Element; - // Warning: (ae-missing-release-tag) "searchPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) const searchPlugin: BackstagePlugin< { root: RouteRef; - nextRoot: RouteRef; }, {} >; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 2eb2ed277b..bcb8f40506 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -60,9 +60,7 @@ export type { SidebarSearchModalProps } from './components/SidebarSearchModal'; export { DefaultResultListItem, HomePageSearchBar, - SearchBarNext, SearchPage, - SearchPageNext, searchPlugin as plugin, searchPlugin, SearchResult, diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 96e7bc1eed..e535295604 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -30,10 +30,6 @@ export const rootRouteRef = createRouteRef({ id: 'search', }); -export const rootNextRouteRef = createRouteRef({ - id: 'search:next', -}); - export const searchPlugin = createPlugin({ id: 'search', apis: [ @@ -47,7 +43,6 @@ export const searchPlugin = createPlugin({ ], routes: { root: rootRouteRef, - nextRoot: rootNextRouteRef, }, }); @@ -59,20 +54,6 @@ export const SearchPage = searchPlugin.provide( }), ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -export const SearchPageNext = searchPlugin.provide( - createRoutableExtension({ - name: 'SearchPageNext', - component: () => import('./components/SearchPage').then(m => m.SearchPage), - mountPoint: rootNextRouteRef, - }), -); - export const SearchBar = searchPlugin.provide( createComponentExtension({ name: 'SearchBar', @@ -82,21 +63,6 @@ export const SearchBar = searchPlugin.provide( }), ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -export const SearchBarNext = searchPlugin.provide( - createComponentExtension({ - name: 'SearchBarNext', - component: { - lazy: () => import('./components/SearchBar').then(m => m.SearchBar), - }, - }), -); - export const SearchResult = searchPlugin.provide( createComponentExtension({ name: 'SearchResult', @@ -106,21 +72,6 @@ export const SearchResult = searchPlugin.provide( }), ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -export const SearchResultNext = searchPlugin.provide( - createComponentExtension({ - name: 'SearchResultNext', - component: { - lazy: () => import('./components/SearchResult').then(m => m.SearchResult), - }, - }), -); - export const SidebarSearchModal = searchPlugin.provide( createComponentExtension({ name: 'SidebarSearchModal', From bc6ce63e7bf5794cddf976e9e7eb8897d71a6f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 09:41:14 +0200 Subject: [PATCH 29/54] Move DefaultResultListItem from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search, and properly deprecate SearchResult in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 28 +++- plugins/search-react/package.json | 2 +- .../DefaultResultListItem.stories.tsx | 2 +- .../DefaultResultListItem.test.tsx | 2 +- .../DefaultResultListItem.tsx | 43 +++++- .../components/DefaultResultListItem/index.ts | 3 +- .../SearchResult/SearchResult.stories.tsx | 9 +- .../SearchResult/SearchResult.test.tsx | 6 +- .../components/SearchResult/SearchResult.tsx | 29 +++- .../src/components/SearchResult/index.tsx | 2 +- plugins/search-react/src/components/index.ts | 1 + plugins/search/api-report.md | 31 ++--- .../SearchModal/SearchModal.stories.tsx | 4 +- .../components/SearchModal/SearchModal.tsx | 4 +- .../SearchResult/SearchResult.test.tsx | 125 ------------------ .../components/SearchResult/SearchResult.tsx | 57 -------- .../src/components/SearchResult/index.tsx | 17 --- plugins/search/src/plugin.ts | 33 ++--- 18 files changed, 131 insertions(+), 267 deletions(-) rename plugins/{search => search-react}/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx (98%) rename plugins/{search => search-react}/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx (98%) rename plugins/{search => search-react}/src/components/DefaultResultListItem/DefaultResultListItem.tsx (71%) rename plugins/{search => search-react}/src/components/DefaultResultListItem/index.ts (84%) delete mode 100644 plugins/search/src/components/SearchResult/SearchResult.test.tsx delete mode 100644 plugins/search/src/components/SearchResult/SearchResult.tsx delete mode 100644 plugins/search/src/components/SearchResult/index.tsx diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 27279e09a9..c36e7c67fe 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -12,6 +12,9 @@ import { JsonObject } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; +import { ReactNode } from 'react'; +import { ResultHighlight } from '@backstage/plugin-search-common'; +import { SearchDocument } from '@backstage/plugin-search-common'; import { SearchQuery } from '@backstage/plugin-search-common'; import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; @@ -24,6 +27,20 @@ export const AutocompleteFilter: ( // @public (undocumented) export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; +// @public +export const DefaultResultListItem: ( + props: DefaultResultListItemProps, +) => JSX.Element; + +// @public +export type DefaultResultListItemProps = { + icon?: ReactNode; + secondaryAction?: ReactNode; + result: SearchDocument; + highlight?: ResultHighlight; + lineClamp?: number; +}; + // @public (undocumented) export const HighlightedSearchResultText: ({ text, @@ -151,13 +168,18 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -// @public (undocumented) -export const SearchResult: ({ children }: SearchResultProps) => JSX.Element; +// @public +export const SearchResult: (props: SearchResultProps) => JSX.Element; + +// @public +export const SearchResultComponent: ({ + children, +}: SearchResultProps) => JSX.Element; // @public (undocumented) export const SearchResultPager: () => JSX.Element; -// @public (undocumented) +// @public export type SearchResultProps = { children: (results: { results: SearchResult_2[] }) => JSX.Element; }; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 95507e64b7..60cdc479bb 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -31,11 +31,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/plugin-search": "0.8.2-next.1", "@backstage/plugin-search-common": "^0.3.5-next.0", "@backstage/core-components": "^0.9.5-next.1", "@backstage/core-plugin-api": "^1.0.3-next.0", "@backstage/version-bridge": "^1.0.1", + "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx similarity index 98% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx rename to plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index fb56d1ecdb..3b54e69a19 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx similarity index 98% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx rename to plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx index 26d081b3b6..e94d394639 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx similarity index 71% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx rename to plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 5ce2d0744a..48f12213e4 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -15,11 +15,12 @@ */ import React, { ReactNode } from 'react'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { ResultHighlight, SearchDocument, } from '@backstage/plugin-search-common'; -import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; +import { HighlightedSearchResultText } from '../HighlightedSearchResultText'; import { ListItem, ListItemIcon, @@ -29,7 +30,12 @@ import { } from '@material-ui/core'; import { Link } from '@backstage/core-components'; -type Props = { +/** + * Props for {@link DefaultResultListItem} + * + * @public + */ +export type DefaultResultListItemProps = { icon?: ReactNode; secondaryAction?: ReactNode; result: SearchDocument; @@ -37,13 +43,18 @@ type Props = { lineClamp?: number; }; -export const DefaultResultListItem = ({ +/** + * A default result list item. + * + * @public + */ +export const DefaultResultListItemComponent = ({ result, highlight, icon, secondaryAction, lineClamp = 5, -}: Props) => { +}: DefaultResultListItemProps) => { return ( @@ -88,3 +99,25 @@ export const DefaultResultListItem = ({ ); }; + +/** + * A higher order function providing AnalyticsContext to the DefaultResultListItemComponent. + * + * @public + */ +const HigherOrderDefaultResultListItem = ( + props: DefaultResultListItemProps, +) => { + return ( + + + + ); +}; + +export { HigherOrderDefaultResultListItem as DefaultResultListItem }; diff --git a/plugins/search/src/components/DefaultResultListItem/index.ts b/plugins/search-react/src/components/DefaultResultListItem/index.ts similarity index 84% rename from plugins/search/src/components/DefaultResultListItem/index.ts rename to plugins/search-react/src/components/DefaultResultListItem/index.ts index 77f975a9ef..a555f25c02 100644 --- a/plugins/search/src/components/DefaultResultListItem/index.ts +++ b/plugins/search-react/src/components/DefaultResultListItem/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -15,3 +15,4 @@ */ export { DefaultResultListItem } from './DefaultResultListItem'; +export type { DefaultResultListItemProps } from './DefaultResultListItem'; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index 38be586f97..32f9a19a07 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -14,16 +14,17 @@ * limitations under the License. */ -import { Link } from '@backstage/core-components'; -import { List, ListItem } from '@material-ui/core'; import React, { ComponentType } from 'react'; +import { List, ListItem } from '@material-ui/core'; import { MemoryRouter } from 'react-router'; -import { DefaultResultListItem } from '@backstage/plugin-search'; + +import { Link } from '@backstage/core-components'; +import { TestApiProvider } from '@backstage/test-utils'; import { searchApiRef, MockSearchApi } from '../../api'; import { SearchContextProvider } from '../../context'; +import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchResult } from './SearchResult'; -import { TestApiProvider } from '@backstage/test-utils'; const mockResults = { results: [ diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx index 10437b3af7..988d59d7cf 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; import React from 'react'; +import { waitFor } from '@testing-library/react'; + +import { renderInTestApp } from '@backstage/test-utils'; + import { useSearch } from '../../context'; import { SearchResult } from './SearchResult'; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index a1ce3cea25..54024d546a 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -14,16 +14,21 @@ * limitations under the License. */ +import React from 'react'; + import { EmptyState, Progress, ResponseErrorPanel, } from '@backstage/core-components'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { SearchResult } from '@backstage/plugin-search-common'; -import React from 'react'; + import { useSearch } from '../../context'; /** + * Props for {@link SearchResultComponent} + * * @public */ export type SearchResultProps = { @@ -31,6 +36,8 @@ export type SearchResultProps = { }; /** + * A component returning the search result. + * * @public */ export const SearchResultComponent = ({ children }: SearchResultProps) => { @@ -57,4 +64,22 @@ export const SearchResultComponent = ({ children }: SearchResultProps) => { return <>{children({ results: value.results })}; }; -export { SearchResultComponent as SearchResult }; +/** + * A higher order function providing AnalyticsContext to the SearchResultComponent. + * + * @public + */ +const HigherOrderSearchResult = (props: SearchResultProps) => { + return ( + + + + ); +}; + +export { HigherOrderSearchResult as SearchResult }; diff --git a/plugins/search-react/src/components/SearchResult/index.tsx b/plugins/search-react/src/components/SearchResult/index.tsx index 2032fbbed9..503ac47bbd 100644 --- a/plugins/search-react/src/components/SearchResult/index.tsx +++ b/plugins/search-react/src/components/SearchResult/index.tsx @@ -14,5 +14,5 @@ * limitations under the License. */ -export { SearchResult } from './SearchResult'; +export { SearchResult, SearchResultComponent } from './SearchResult'; export type { SearchResultProps } from './SearchResult'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 5b7767859e..155a4c3b12 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -20,3 +20,4 @@ export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchBar'; export * from './SearchTracker'; +export * from './DefaultResultListItem'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 0527c46fe8..746e23e880 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -6,33 +6,22 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DefaultResultListItemProps } from '@backstage/plugin-search-react/src/components/DefaultResultListItem/DefaultResultListItem'; import { IconComponent } from '@backstage/core-plugin-api'; import { InputBaseProps } from '@material-ui/core'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; -import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react'; -import { SearchDocument } from '@backstage/plugin-search-common'; import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; -import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; +import { SearchResultProps } from '@backstage/plugin-search-react'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const DefaultResultListItem: ({ - result, - highlight, - icon, - secondaryAction, - lineClamp, -}: { - icon?: ReactNode; - secondaryAction?: ReactNode; - result: SearchDocument; - highlight?: ResultHighlight | undefined; - lineClamp?: number | undefined; -}) => JSX.Element; +// @public @deprecated (undocumented) +export const DefaultResultListItem: ( + props: DefaultResultListItemProps, +) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "FiltersProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Filters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -214,12 +203,8 @@ export { searchPlugin }; // Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const SearchResult: ({ - children, -}: { - children: (results: { results: SearchResult_2[] }) => JSX.Element; -}) => JSX.Element; +// @public @deprecated (undocumented) +export const SearchResult: (props: SearchResultProps) => JSX.Element; // Warning: (ae-missing-release-tag) "SearchResultPager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index a4e92f53ec..870f73e472 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -27,16 +27,16 @@ import { import { makeStyles } from '@material-ui/core/styles'; import React, { ComponentType } from 'react'; import { rootRouteRef } from '../../plugin'; -import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchBar } from '../SearchBar'; import { + DefaultResultListItem, searchApiRef, MockSearchApi, SearchContextProvider, + SearchResult, } from '@backstage/plugin-search-react'; import { TestApiProvider } from '@backstage/test-utils'; import { SearchModal } from './SearchModal'; -import { SearchResult } from '../SearchResult'; import { SearchResultPager } from '../SearchResultPager'; import { SearchType } from '../SearchType'; import { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index b3df52d42e..18776d444d 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -29,10 +29,10 @@ import { import LaunchIcon from '@material-ui/icons/Launch'; import { makeStyles } from '@material-ui/core/styles'; import { SearchBar } from '../SearchBar'; -import { DefaultResultListItem } from '../DefaultResultListItem'; -import { SearchResult } from '../SearchResult'; import { + DefaultResultListItem, SearchContextProvider, + SearchResult, useSearch, } from '@backstage/plugin-search-react'; import { SearchResultPager } from '../SearchResultPager'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.test.tsx b/plugins/search/src/components/SearchResult/SearchResult.test.tsx deleted file mode 100644 index d93eacaef0..0000000000 --- a/plugins/search/src/components/SearchResult/SearchResult.test.tsx +++ /dev/null @@ -1,125 +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 { renderInTestApp } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; -import React from 'react'; -import { useSearch } from '@backstage/plugin-search-react'; -import { SearchResult } from './SearchResult'; - -jest.mock('@backstage/plugin-search-react', () => ({ - ...jest.requireActual('@backstage/plugin-search-react'), - useSearch: jest.fn().mockReturnValue({ - result: {}, - }), -})); - -describe('SearchResult', () => { - it('Progress rendered on Loading state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: true }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect(getByRole('progressbar')).toBeInTheDocument(); - }); - }); - - it('Alert rendered on Error state', async () => { - const error = new Error('some error'); - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: false, error }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect(getByRole('alert')).toHaveTextContent( - new RegExp(`Error encountered while fetching search results.*${error}`), - ); - }); - }); - - it('On no result value state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: false, error: '', value: undefined }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect( - getByRole('heading', { name: 'Sorry, no results were found' }), - ).toBeInTheDocument(); - }); - }); - - it('On empty result value state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: false, error: '', value: { results: [] } }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect( - getByRole('heading', { name: 'Sorry, no results were found' }), - ).toBeInTheDocument(); - }); - }); - - it('Calls children with results set to result.value', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { - loading: false, - error: '', - value: { - totalCount: 1, - results: [ - { - type: 'some-type', - document: { - title: 'some-title', - text: 'some-text', - location: 'some-location', - }, - }, - ], - }, - }, - }); - - const { getByText } = await renderInTestApp( - - {({ results }) => { - return <>Results {results.length}; - }} - , - ); - - expect(getByText('Results 1')).toBeInTheDocument(); - }); -}); diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx deleted file mode 100644 index 77ee883164..0000000000 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ /dev/null @@ -1,57 +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 { - EmptyState, - Progress, - ResponseErrorPanel, -} from '@backstage/core-components'; -import { SearchResult } from '@backstage/plugin-search-common'; -import React from 'react'; -import { useSearch } from '@backstage/plugin-search-react'; - -type Props = { - children: (results: { results: SearchResult[] }) => JSX.Element; -}; - -/** - * @deprecated Moved to `@backstage/plugin-search-react`. - */ -export const SearchResultComponent = ({ children }: Props) => { - const { - result: { loading, error, value }, - } = useSearch(); - - if (loading) { - return ; - } - if (error) { - return ( - - ); - } - - if (!value?.results.length) { - return ; - } - - return <>{children({ results: value.results })}; -}; - -export { SearchResultComponent as SearchResult }; diff --git a/plugins/search/src/components/SearchResult/index.tsx b/plugins/search/src/components/SearchResult/index.tsx deleted file mode 100644 index e7d088f286..0000000000 --- a/plugins/search/src/components/SearchResult/index.tsx +++ /dev/null @@ -1,17 +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. - */ - -export { SearchResult } from './SearchResult'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index e535295604..fdfa2f59d0 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -15,7 +15,11 @@ */ import { SearchClient } from './apis'; -import { searchApiRef } from '@backstage/plugin-search-react'; +import { + searchApiRef, + SearchResult as RealSearchResult, + DefaultResultListItem as RealDefaultResultListItem, +} from '@backstage/plugin-search-react'; import { createApiFactory, createPlugin, @@ -63,14 +67,10 @@ export const SearchBar = searchPlugin.provide( }), ); -export const SearchResult = searchPlugin.provide( - createComponentExtension({ - name: 'SearchResult', - component: { - lazy: () => import('./components/SearchResult').then(m => m.SearchResult), - }, - }), -); +/** + * @deprecated Import from `@backstage/plugin-search-react` instead. + */ +export const SearchResult = RealSearchResult; export const SidebarSearchModal = searchPlugin.provide( createComponentExtension({ @@ -84,17 +84,10 @@ export const SidebarSearchModal = searchPlugin.provide( }), ); -export const DefaultResultListItem = searchPlugin.provide( - createComponentExtension({ - name: 'DefaultResultListItem', - component: { - lazy: () => - import('./components/DefaultResultListItem').then( - m => m.DefaultResultListItem, - ), - }, - }), -); +/** + * @deprecated Import from `@backstage/plugin-search-react` instead. + */ +export const DefaultResultListItem = RealDefaultResultListItem; export const HomePageSearchBar = searchPlugin.provide( createComponentExtension({ From a7a0e2520ae5ccc0b699ea64a37d7d3817db661d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 09:44:22 +0200 Subject: [PATCH 30/54] cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search/api-report.md | 2 +- plugins/search/src/plugin.ts | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 746e23e880..3cbbf8072c 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -6,7 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { DefaultResultListItemProps } from '@backstage/plugin-search-react/src/components/DefaultResultListItem/DefaultResultListItem'; +import { DefaultResultListItemProps } from '@backstage/plugin-search-react'; import { IconComponent } from '@backstage/core-plugin-api'; import { InputBaseProps } from '@material-ui/core'; import { ReactElement } from 'react'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index fdfa2f59d0..103d67b81a 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -58,15 +58,6 @@ export const SearchPage = searchPlugin.provide( }), ); -export const SearchBar = searchPlugin.provide( - createComponentExtension({ - name: 'SearchBar', - component: { - lazy: () => import('./components/SearchBar').then(m => m.SearchBar), - }, - }), -); - /** * @deprecated Import from `@backstage/plugin-search-react` instead. */ From b3d64b448ec9f9f9e82ac52c4335f684dcb041e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 11:35:52 +0200 Subject: [PATCH 31/54] api-report cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 2 +- .../HighlightedSearchResultText.tsx | 2 + plugins/search/api-report.md | 73 +++++++------------ .../search/src/components/Filters/Filters.tsx | 16 +++- .../src/components/Filters/FiltersButton.tsx | 10 ++- .../search/src/components/Filters/index.tsx | 3 +- .../HomePageComponent/HomePageSearchBar.tsx | 4 +- .../src/components/SearchBar/SearchBar.tsx | 12 +-- .../SearchFilter.Autocomplete.tsx | 4 +- .../components/SearchFilter/SearchFilter.tsx | 11 ++- .../components/SearchModal/SearchModal.tsx | 6 ++ .../src/components/SearchPage/SearchPage.tsx | 3 + .../SearchResultPager/SearchResultPager.tsx | 1 + .../src/components/SearchType/SearchType.tsx | 5 ++ .../SidebarSearch/SidebarSearch.tsx | 8 ++ .../SidebarSearchModal/SidebarSearchModal.tsx | 5 ++ plugins/search/src/index.ts | 7 +- plugins/search/src/plugin.ts | 14 ++++ 18 files changed, 116 insertions(+), 70 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index c36e7c67fe..93ed657d85 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -48,7 +48,7 @@ export const HighlightedSearchResultText: ({ postTag, }: HighlightedSearchResultTextProps) => JSX.Element; -// @public (undocumented) +// @public export type HighlightedSearchResultTextProps = { text: string; preTag: string; diff --git a/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx b/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx index a749f184fe..bd6ef2d09c 100644 --- a/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx +++ b/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx @@ -25,6 +25,8 @@ const useStyles = makeStyles( ); /** + * Props for {@link HighlightedSearchResultText}. + * * @public */ export type HighlightedSearchResultTextProps = { diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 3cbbf8072c..c2f1e93bff 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -16,16 +16,17 @@ import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; import { SearchResultProps } from '@backstage/plugin-search-react'; -// Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, ) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "FiltersProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Filters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type FilterOptions = { + kind: Array; + lifecycle: Array; +}; + // @public (undocumented) export const Filters: ({ filters, @@ -35,25 +36,33 @@ export const Filters: ({ updateChecked, }: FiltersProps) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "FiltersButtonProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FiltersButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const FiltersButton: ({ numberOfSelectedFilters, handleToggleFilters, }: FiltersButtonProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "FiltersState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type FiltersButtonProps = { + numberOfSelectedFilters: number; + handleToggleFilters: () => void; +}; + +// @public +export type FiltersProps = { + filters: FiltersState; + filterOptions: FilterOptions; + resetFilters: () => void; + updateSelected: (filter: string) => void; + updateChecked: (filter: string) => void; +}; + // @public (undocumented) export type FiltersState = { selected: string; checked: Array; }; -// Warning: (ae-missing-release-tag) "HomePageSearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const HomePageSearchBar: ({ ...props @@ -64,8 +73,6 @@ export type HomePageSearchBarProps = Partial< Omit >; -// Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const Router: () => JSX.Element; @@ -76,10 +83,10 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & { multiple?: boolean; }; -// @public @deprecated (undocumented) +// @public @deprecated export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; -// @public @deprecated (undocumented) +// @public @deprecated export const SearchBarBase: ({ onChange, onKeyDown, @@ -93,7 +100,7 @@ export const SearchBarBase: ({ ...props }: SearchBarBaseProps) => JSX.Element; -// @public @deprecated (undocumented) +// @public @deprecated export type SearchBarBaseProps = Omit & { debounceTime?: number; clearButton?: boolean; @@ -102,11 +109,9 @@ export type SearchBarBaseProps = Omit & { onChange: (value: string) => void; }; -// @public @deprecated (undocumented) +// @public @deprecated export type SearchBarProps = Partial; -// Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const SearchFilter: { ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; @@ -137,8 +142,6 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -// Warning: (ae-missing-release-tag) "SearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SearchModal: ({ open, @@ -152,8 +155,6 @@ export interface SearchModalChildrenProps { toggleModal: () => void; } -// Warning: (ae-missing-release-tag) "SearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface SearchModalProps { children?: (props: SearchModalChildrenProps) => JSX.Element; @@ -184,13 +185,9 @@ export type SearchModalValue = { setOpen: (open: boolean) => void; }; -// Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SearchPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "searchPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) const searchPlugin: BackstagePlugin< { @@ -201,18 +198,12 @@ const searchPlugin: BackstagePlugin< export { searchPlugin as plugin }; export { searchPlugin }; -// Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const SearchResult: (props: SearchResultProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchResultPager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const SearchResultPager: () => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SearchType: { (props: SearchTypeProps): JSX.Element; @@ -231,7 +222,7 @@ export type SearchTypeAccordionProps = { defaultValue?: string; }; -// @public (undocumented) +// @public export type SearchTypeProps = { className?: string; name: string; @@ -248,29 +239,21 @@ export type SearchTypeTabsProps = { defaultValue?: string; }; -// Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SidebarSearch: (props: SidebarSearchProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "SidebarSearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SidebarSearchModal: ( props: SidebarSearchModalProps, ) => JSX.Element; -// Warning: (ae-missing-release-tag) "SidebarSearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SidebarSearchModalProps = { icon?: IconComponent; children?: (props: SearchModalChildrenProps) => JSX.Element; }; -// Warning: (ae-missing-release-tag) "SidebarSearchProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SidebarSearchProps = { icon?: IconComponent; }; diff --git a/plugins/search/src/components/Filters/Filters.tsx b/plugins/search/src/components/Filters/Filters.tsx index a0ca4914a2..9bbe90d32f 100644 --- a/plugins/search/src/components/Filters/Filters.tsx +++ b/plugins/search/src/components/Filters/Filters.tsx @@ -44,17 +44,28 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * @public + */ export type FiltersState = { selected: string; checked: Array; }; +/** + * @public + */ export type FilterOptions = { kind: Array; lifecycle: Array; }; -type FiltersProps = { +/** + * Props for {@link Filters}. + * + * @public + */ +export type FiltersProps = { filters: FiltersState; filterOptions: FilterOptions; resetFilters: () => void; @@ -62,6 +73,9 @@ type FiltersProps = { updateChecked: (filter: string) => void; }; +/** + * @public + */ export const Filters = ({ filters, filterOptions, diff --git a/plugins/search/src/components/Filters/FiltersButton.tsx b/plugins/search/src/components/Filters/FiltersButton.tsx index 1c2829defe..8d896bbf30 100644 --- a/plugins/search/src/components/Filters/FiltersButton.tsx +++ b/plugins/search/src/components/Filters/FiltersButton.tsx @@ -28,11 +28,19 @@ const useStyles = makeStyles(theme => ({ }, })); -type FiltersButtonProps = { +/** + * Props for {@link FiltersButton}. + * + * @public + */ +export type FiltersButtonProps = { numberOfSelectedFilters: number; handleToggleFilters: () => void; }; +/** + * @public + */ export const FiltersButton = ({ numberOfSelectedFilters, handleToggleFilters, diff --git a/plugins/search/src/components/Filters/index.tsx b/plugins/search/src/components/Filters/index.tsx index de890e0eaf..dc4d811dbd 100644 --- a/plugins/search/src/components/Filters/index.tsx +++ b/plugins/search/src/components/Filters/index.tsx @@ -15,5 +15,6 @@ */ export { FiltersButton } from './FiltersButton'; +export type { FiltersButtonProps } from './FiltersButton'; export { Filters } from './Filters'; -export type { FiltersState } from './Filters'; +export type { FilterOptions, FiltersProps, FiltersState } from './Filters'; diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index bc4b409d57..b4e661b916 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -37,9 +37,7 @@ export type HomePageSearchBarProps = Partial< >; /** - * The search bar created specifically for the composable home page - * - * @public + * The search bar created specifically for the composable home page. */ export const HomePageSearchBar = ({ ...props }: HomePageSearchBarProps) => { const classes = useStyles(props); diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index cb61089eb6..587478ed46 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -40,11 +40,10 @@ import { } from '@backstage/plugin-search-react'; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * Props for {@link SearchBarBase}. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchBarBaseProps = Omit & { debounceTime?: number; @@ -55,13 +54,12 @@ export type SearchBarBaseProps = Omit & { }; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * All search boxes exported by the search plugin are based on the , * and this one is based on the component from Material UI. * Recommended if you don't use Search Provider or Search Context. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export const SearchBarBase = ({ onChange, @@ -153,20 +151,18 @@ export const SearchBarBase = ({ }; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * Props for {@link SearchBar}. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchBarProps = Partial; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * Recommended search bar when you use the Search Provider or Search Context. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index 1a74decd31..ac37e3bd2c 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -29,9 +29,8 @@ import { } from '@backstage/plugin-search-react'; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { filterSelectedOptions?: boolean; @@ -41,7 +40,6 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { /** * @deprecated Moved to `@backstage/plugin-search-react`. - * */ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { const { diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index b88160f363..e7982ca5c4 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -24,9 +24,8 @@ import { } from '@backstage/plugin-search-react'; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchFilterComponentProps = { className?: string; @@ -48,9 +47,8 @@ export type SearchFilterComponentProps = { }; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchFilterWrapperProps = SearchFilterComponentProps & { component: (props: SearchFilterComponentProps) => ReactElement; @@ -58,6 +56,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { }; /** + * @public * @deprecated Moved to `@backstage/plugin-search-react`. */ const SearchFilter = ({ @@ -82,12 +81,12 @@ SearchFilter.Select = ( ) => ; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * A control surface for a given filter field name, rendered as an autocomplete * textfield. A hard-coded list of values may be provided, or an async function * which returns values may be provided instead. + * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 18776d444d..ebc2979acc 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -50,6 +50,9 @@ export interface SearchModalChildrenProps { toggleModal: () => void; } +/** + * @public + **/ export interface SearchModalProps { /** * If true, it renders the modal. @@ -167,6 +170,9 @@ export const Modal = ({ toggleModal }: SearchModalProps) => { ); }; +/** + * @public + */ export const SearchModal = ({ open = true, hidden, diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index 29a22a6f5c..7145c7a5ef 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -87,6 +87,9 @@ export const UrlUpdater = () => { return null; }; +/** + * @public + */ export const SearchPage = () => { const outlet = useOutlet(); diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index 0e8d59a694..f99b6cf29f 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -30,6 +30,7 @@ const useStyles = makeStyles(theme => ({ })); /** + * @public * @deprecated Moved to `@backstage/plugin-search-react`. */ export const SearchResultPager = () => { diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx index 79c3dda2b9..b20e8b5fba 100644 --- a/plugins/search/src/components/SearchType/SearchType.tsx +++ b/plugins/search/src/components/SearchType/SearchType.tsx @@ -47,6 +47,8 @@ const useStyles = makeStyles(theme => ({ })); /** + * Props for {@link SearchType}. + * * @public */ export type SearchTypeProps = { @@ -56,6 +58,9 @@ export type SearchTypeProps = { defaultValue?: string[] | string | null; }; +/** + * @public + */ const SearchType = (props: SearchTypeProps) => { const { className, defaultValue, name, values = [] } = props; const classes = useStyles(); diff --git a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx index 25d41fa10f..36ead2ab15 100644 --- a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx +++ b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx @@ -21,10 +21,18 @@ import { rootRouteRef } from '../../plugin'; import { useRouteRef, IconComponent } from '@backstage/core-plugin-api'; import { SidebarSearchField, useContent } from '@backstage/core-components'; +/** + * Props for {@link SidebarSearch}. + * + * @public + */ export type SidebarSearchProps = { icon?: IconComponent; }; +/** + * @public + */ export const SidebarSearch = (props: SidebarSearchProps) => { const searchRoute = useRouteRef(rootRouteRef); const { focusContent } = useContent(); diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index 191a09395c..fb4afc75fd 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -24,6 +24,11 @@ import { useSearchModal, } from '../SearchModal'; +/** + * Props for {@link SidebarSearchModal}. + * + * @public + */ export type SidebarSearchModalProps = { icon?: IconComponent; children?: (props: SearchModalChildrenProps) => JSX.Element; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index bcb8f40506..fd0e70c61a 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -21,7 +21,12 @@ */ export { Filters, FiltersButton } from './components/Filters'; -export type { FiltersState } from './components/Filters'; +export type { + FilterOptions, + FiltersState, + FiltersProps, + FiltersButtonProps, +} from './components/Filters'; export type { HomePageSearchBarProps } from './components/HomePageComponent'; export { SearchBar, SearchBarBase } from './components/SearchBar'; export type { diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 103d67b81a..19fd501129 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -34,6 +34,9 @@ export const rootRouteRef = createRouteRef({ id: 'search', }); +/** + * @public + */ export const searchPlugin = createPlugin({ id: 'search', apis: [ @@ -50,6 +53,9 @@ export const searchPlugin = createPlugin({ }, }); +/** + * @public + */ export const SearchPage = searchPlugin.provide( createRoutableExtension({ name: 'SearchPage', @@ -59,10 +65,14 @@ export const SearchPage = searchPlugin.provide( ); /** + * @public * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const SearchResult = RealSearchResult; +/** + * @public + */ export const SidebarSearchModal = searchPlugin.provide( createComponentExtension({ name: 'SidebarSearchModal', @@ -76,10 +86,14 @@ export const SidebarSearchModal = searchPlugin.provide( ); /** + * @public * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const DefaultResultListItem = RealDefaultResultListItem; +/** + * @public + */ export const HomePageSearchBar = searchPlugin.provide( createComponentExtension({ name: 'HomePageSearchBar', From 1b1c47f4a774ec9b6f116d6de7e8bfcbffb94f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 12:01:12 +0200 Subject: [PATCH 32/54] update imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- packages/app/src/components/search/SearchModal.tsx | 9 ++++----- packages/app/src/components/search/SearchPage.tsx | 6 +++--- .../packages/app/src/components/search/SearchPage.tsx | 8 ++++---- .../components/HomePageComponent/HomePageSearchBar.tsx | 5 ++++- .../src/components/SearchModal/SearchModal.stories.tsx | 2 +- .../search/src/components/SearchModal/SearchModal.tsx | 4 ++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx index 4af5511443..f5f4204302 100644 --- a/packages/app/src/components/search/SearchModal.tsx +++ b/packages/app/src/components/search/SearchModal.tsx @@ -38,16 +38,15 @@ import { catalogApiRef, CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; +import { searchPlugin, SearchType } from '@backstage/plugin-search'; import { DefaultResultListItem, - SearchBar, SearchFilter, - searchPlugin, + SearchBar, SearchResult, SearchResultPager, - SearchType, -} from '@backstage/plugin-search'; -import { useSearch } from '@backstage/plugin-search-react'; + useSearch, +} from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; const useStyles = makeStyles(theme => ({ diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index ee144b2cde..9a2e85bd8c 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -29,15 +29,15 @@ import { catalogApiRef, CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; +import { SearchType } from '@backstage/plugin-search'; import { DefaultResultListItem, SearchBar, SearchFilter, SearchResult, SearchResultPager, - SearchType, -} from '@backstage/plugin-search'; -import { useSearch } from '@backstage/plugin-search-react'; + useSearch, +} from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; import React from 'react'; diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index d4c7c92346..08166bb308 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -8,14 +8,14 @@ import { } from '@backstage/plugin-catalog-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; +import { SearchType } from '@backstage/plugin-search'; import { + DefaultResultListItem, SearchBar, SearchFilter, SearchResult, - SearchType, - DefaultResultListItem, -} from '@backstage/plugin-search'; -import { useSearch } from '@backstage/plugin-search-react'; + useSearch +} from '@backstage/plugin-search-react'; import { CatalogIcon, Content, diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index b4e661b916..6e9787c2e0 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -16,7 +16,10 @@ import React, { useCallback, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { SearchBarBase, SearchBarBaseProps } from '../SearchBar'; +import { + SearchBarBase, + SearchBarBaseProps, +} from '@backstage/plugin-search-react'; import { useNavigateToQuery } from '../util'; const useStyles = makeStyles({ diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index 870f73e472..aad018c334 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -34,10 +34,10 @@ import { MockSearchApi, SearchContextProvider, SearchResult, + SearchResultPager, } from '@backstage/plugin-search-react'; import { TestApiProvider } from '@backstage/test-utils'; import { SearchModal } from './SearchModal'; -import { SearchResultPager } from '../SearchResultPager'; import { SearchType } from '../SearchType'; import { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index ebc2979acc..6c37f20001 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -28,14 +28,14 @@ import { } from '@material-ui/core'; import LaunchIcon from '@material-ui/icons/Launch'; import { makeStyles } from '@material-ui/core/styles'; -import { SearchBar } from '../SearchBar'; import { DefaultResultListItem, SearchContextProvider, + SearchBar, SearchResult, + SearchResultPager, useSearch, } from '@backstage/plugin-search-react'; -import { SearchResultPager } from '../SearchResultPager'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Link, useContent } from '@backstage/core-components'; import { rootRouteRef } from '../../plugin'; From f28bc288341a79f620149475e998df69da7a739a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 13:09:59 +0200 Subject: [PATCH 33/54] update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- docs/assets/search/architecture.drawio.svg | 148 ++++++++++++++++----- docs/features/search/getting-started.md | 8 +- plugins/search/README.md | 13 +- 3 files changed, 127 insertions(+), 42 deletions(-) diff --git a/docs/assets/search/architecture.drawio.svg b/docs/assets/search/architecture.drawio.svg index 709a008b43..5c7c98ebbf 100644 --- a/docs/assets/search/architecture.drawio.svg +++ b/docs/assets/search/architecture.drawio.svg @@ -1,17 +1,36 @@ - + + + + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
- + -
+
@@ -21,7 +40,7 @@
- + @@ -103,11 +122,11 @@ - + -
+
@backstage/plugin-xyz @@ -115,7 +134,7 @@
- + @backstage/plugin-xyz @@ -251,10 +270,7 @@ - - - - + @@ -274,11 +290,14 @@ - + + + + -
+
@@ -288,18 +307,18 @@
- + Search API - - + + -
+
@@ -309,7 +328,7 @@
- + Components @@ -824,11 +843,11 @@ - + -
+
@@ -844,16 +863,19 @@
- + Individual frontend... - + + + + -
+
@@ -863,20 +885,80 @@
- + Search Cont... - - - - - - - - - + + + + + + + + + + + + + +
+
+
+ @backstage/plugin-search-react +
+
+
+
+ + @backstage/plugin-search-react + +
+
+ + + + + + + + +
+
+
+ + Components + +
+
+
+
+ + Components + +
+
+ + + + + +
+
+
+ + Search Client + +
+
+
+
+ + Search Client + +
+
diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 14ad523740..d0e13eda55 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -18,7 +18,7 @@ If you haven't setup Backstage already, start ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-search +yarn add --cwd packages/app @backstage/plugin-search @backstage/plugin-search-react ``` Create a new `packages/app/src/components/search/SearchPage.tsx` file in your @@ -33,7 +33,7 @@ import { SearchResult, DefaultResultListItem, SearchFilter, -} from '@backstage/plugin-search'; +} from '@backstage/plugin-search-react'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; export const searchPage = ( @@ -213,7 +213,7 @@ apiRouter.use('/search', await search(searchEnv)); ### Frontend -The Search Plugin exposes several default filter types as static properties, +The Search Plugin web library (`@backstage/plugin-search-react`) exposes several default filter types as static properties, including `` and ``. These allow you to provide values relevant to your Backstage instance that, when selected, get passed to the backend. @@ -237,7 +237,7 @@ If you have advanced filter needs, you can specify your own filter component like this (although new core filter contributions are welcome): ```tsx -import { useSearch, SearchFilter } from '@backstage/plugin-search'; +import { useSearch, SearchFilter } from '@backstage/plugin-search-react'; const MyCustomFilter = () => { // Note: filters contain filter data from other filter components. Be sure diff --git a/plugins/search/README.md b/plugins/search/README.md index f4a32c9597..3803b71e43 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -13,14 +13,17 @@ Run `yarn dev` in the root directory, and then navigate to [/search](http://loca This search plugin is primarily responsible for the following: - Providing a `` routable extension. -- Exposing various search-related components (like ``, - ``, etc), which can be composed by a Backstage App or by +- Exposing various search-related components (like ``, + ``, etc), which can be composed by a Backstage App or by other Backstage Plugins to power search experiences of all kinds. -- Exposing a ``, which manages search state and API - communication with the Backstage backend. -Don't forget, a lot of functionality is available in backend plugins: +Don't forget, a lot of functionality is available in web libraries and backend plugins: +- `@backstage/plugin-search-react`, which is responsible for: + - Exposing a ``, which manages search state and API + communication with the Backstage backend. + - Exposing the `SearchApi` and its corresponding ref. + - Exposing reusable components, such as `` and ``, etc. - `@backstage/plugin-search-backend-node`, which is responsible for the search index management - `@backstage/plugin-search-backend`, which is responsible for query processing From 88091591482ddc17044d0a84700acb7457a72a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 13:31:00 +0200 Subject: [PATCH 34/54] changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .changeset/famous-walls-visit.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/famous-walls-visit.md diff --git a/.changeset/famous-walls-visit.md b/.changeset/famous-walls-visit.md new file mode 100644 index 0000000000..8fcaed4ab0 --- /dev/null +++ b/.changeset/famous-walls-visit.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search': minor +'@backstage/plugin-search-react': minor +--- + +Components ``, `` (including ``), `` (including `.Checkbox`, `.Select`, and `.Autocomplete` static prop components), ``, ``, and `` are now exported from `@backstage/plugin-search-react`. They are now deprecated in `@backstage/plugin-search` and will be removed in a future release. From 8c2b28708ffeeeed041073407f08764d441e6767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 13:58:06 +0200 Subject: [PATCH 35/54] update api-report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search/api-report.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index c2f1e93bff..9e418dd08e 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -13,6 +13,7 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react'; +import { SearchBarBaseProps as SearchBarBaseProps_2 } from '@backstage/plugin-search-react'; import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; import { SearchResultProps } from '@backstage/plugin-search-react'; @@ -66,11 +67,11 @@ export type FiltersState = { // @public (undocumented) export const HomePageSearchBar: ({ ...props -}: Partial>) => JSX.Element; +}: Partial>) => JSX.Element; // @public export type HomePageSearchBarProps = Partial< - Omit + Omit >; // @public (undocumented) From 93089a9d063cc2c497904883e34a89e0a0009321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 15:29:29 +0200 Subject: [PATCH 36/54] prettier? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .../packages/app/src/components/search/SearchPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index 08166bb308..1b3bf5b77d 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -14,7 +14,7 @@ import { SearchBar, SearchFilter, SearchResult, - useSearch + useSearch, } from '@backstage/plugin-search-react'; import { CatalogIcon, From 30f04d14972fa3375a0675cc3be287d21ebc39fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 10:16:46 +0200 Subject: [PATCH 37/54] changeset for create-app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .changeset/red-apes-tell.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .changeset/red-apes-tell.md diff --git a/.changeset/red-apes-tell.md b/.changeset/red-apes-tell.md new file mode 100644 index 0000000000..61c4a5077e --- /dev/null +++ b/.changeset/red-apes-tell.md @@ -0,0 +1,24 @@ +--- +'@backstage/create-app': patch +--- + +Components ``, ``, ``, and `` are now deprecated in `@backstage/plugin-search` and should be imported from `@backstage/plugin-search-react` instead. + +To upgrade your App, update the following in `packages/app/src/components/search/SearchPage.tsx`: + +```diff +import { +- SearchBar +- SearchFilter +- SearchResult +SearchType, +- DefaultResultListItem +} from `@backstage/plugin-search`; +import { ++ DefaultResultListItem ++ SearchBar ++ SearchFilter ++ SearchResult +useSearch, +} from `@backstage/plugin-search-react`; +``` From a33399ed59fc863bb789658cdd100bd2266dc1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 10:31:01 +0200 Subject: [PATCH 38/54] fixup to arch diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- docs/assets/search/architecture.drawio.svg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/assets/search/architecture.drawio.svg b/docs/assets/search/architecture.drawio.svg index 5c7c98ebbf..b85541b804 100644 --- a/docs/assets/search/architecture.drawio.svg +++ b/docs/assets/search/architecture.drawio.svg @@ -1,4 +1,4 @@ - + @@ -939,7 +939,6 @@
- From 295eb30778b7892197050f8409001c3d836664e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 16:28:08 +0200 Subject: [PATCH 39/54] remove unnecessary deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .../SearchFilter.Autocomplete.tsx | 100 +----------------- 1 file changed, 2 insertions(+), 98 deletions(-) diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index ac37e3bd2c..3db5112b88 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -14,110 +14,14 @@ * limitations under the License. */ -import React, { ChangeEvent, useState } from 'react'; -import { Chip, TextField } from '@material-ui/core'; -import { - Autocomplete, - AutocompleteGetTagProps, - AutocompleteRenderInputParams, -} from '@material-ui/lab'; -import { - SearchFilterComponentProps, - useSearch, - useAsyncFilterValues, - useDefaultFilterValue, -} from '@backstage/plugin-search-react'; +import { SearchFilterComponentProps } from '@backstage/plugin-search-react'; /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { filterSelectedOptions?: boolean; limitTags?: number; multiple?: boolean; }; - -/** - * @deprecated Moved to `@backstage/plugin-search-react`. - */ -export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { - const { - className, - defaultValue, - name, - values: givenValues, - valuesDebounceMs, - label, - filterSelectedOptions, - limitTags, - multiple, - } = props; - const [inputValue, setInputValue] = useState(''); - useDefaultFilterValue(name, defaultValue); - const asyncValues = - typeof givenValues === 'function' ? givenValues : undefined; - const defaultValues = - typeof givenValues === 'function' ? undefined : givenValues; - const { value: values, loading } = useAsyncFilterValues( - asyncValues, - inputValue, - defaultValues, - valuesDebounceMs, - ); - const { filters, setFilters } = useSearch(); - const filterValue = - (filters[name] as string | string[] | undefined) || (multiple ? [] : null); - - // Set new filter values on input change. - const handleChange = ( - _: ChangeEvent<{}>, - newValue: string | string[] | null, - ) => { - setFilters(prevState => { - const { [name]: filter, ...others } = prevState; - - if (newValue) { - return { ...others, [name]: newValue }; - } - return { ...others }; - }); - }; - - // Provide the input field. - const renderInput = (params: AutocompleteRenderInputParams) => ( - - ); - - // Render tags as primary-colored chips. - const renderTags = ( - tagValue: string[], - getTagProps: AutocompleteGetTagProps, - ) => - tagValue.map((option: string, index: number) => ( - - )); - - return ( - setInputValue(newValue)} - renderInput={renderInput} - renderTags={renderTags} - /> - ); -}; From 2dc4818541f83cabb855d3d160e6691fff1a2f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 16:30:04 +0200 Subject: [PATCH 40/54] apply review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .changeset/famous-walls-visit.md | 6 +- .changeset/red-apes-tell.md | 19 +-- .changeset/strange-flies-march.md | 5 + plugins/search-react/api-report.md | 7 - .../DefaultResultListItem.tsx | 2 - .../components/SearchResult/SearchResult.tsx | 2 - .../SearchTracker/SearchTracker.tsx | 2 - plugins/search-react/src/components/index.ts | 1 - plugins/search/api-report.md | 2 +- .../src/components/SearchBar/SearchBar.tsx | 121 ++---------------- .../components/SearchFilter/SearchFilter.tsx | 12 +- .../SearchResultPager/SearchResultPager.tsx | 2 +- scripts/api-extractor.ts | 1 + 13 files changed, 32 insertions(+), 150 deletions(-) create mode 100644 .changeset/strange-flies-march.md diff --git a/.changeset/famous-walls-visit.md b/.changeset/famous-walls-visit.md index 8fcaed4ab0..b57702a392 100644 --- a/.changeset/famous-walls-visit.md +++ b/.changeset/famous-walls-visit.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-search': minor -'@backstage/plugin-search-react': minor +'@backstage/plugin-search': patch +'@backstage/plugin-search-react': patch --- -Components ``, `` (including ``), `` (including `.Checkbox`, `.Select`, and `.Autocomplete` static prop components), ``, ``, and `` are now exported from `@backstage/plugin-search-react`. They are now deprecated in `@backstage/plugin-search` and will be removed in a future release. +Components ``, `` (including ``), `` (including `.Checkbox`, `.Select`, and `.Autocomplete` static prop components), ``, and `` are now exported from `@backstage/plugin-search-react`. They are now deprecated in `@backstage/plugin-search` and will be removed in a future release. diff --git a/.changeset/red-apes-tell.md b/.changeset/red-apes-tell.md index 61c4a5077e..ee0ace6a4c 100644 --- a/.changeset/red-apes-tell.md +++ b/.changeset/red-apes-tell.md @@ -8,17 +8,10 @@ To upgrade your App, update the following in `packages/app/src/components/search ```diff import { -- SearchBar -- SearchFilter -- SearchResult -SearchType, -- DefaultResultListItem -} from `@backstage/plugin-search`; -import { -+ DefaultResultListItem -+ SearchBar -+ SearchFilter -+ SearchResult -useSearch, -} from `@backstage/plugin-search-react`; + DefaultResultListItem + SearchBar + SearchFilter + SearchResult +- } from `@backstage/plugin-search`; ++ } from `@backstage/plugin-search-react`; ``` diff --git a/.changeset/strange-flies-march.md b/.changeset/strange-flies-march.md new file mode 100644 index 0000000000..70a15b41d9 --- /dev/null +++ b/.changeset/strange-flies-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': minor +--- + +The pre-alpha ``, ``, `etc...` components have been removed. In the unlikely event you were still using/referencing them, please update to using their non-`*Next` equivalents from either `@backstage/plugin-search-react` or `@backstage/plugin-search`. diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 93ed657d85..ce1fa53bb4 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -187,13 +187,6 @@ export type SearchResultProps = { // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; -// @public -export const TrackSearch: ({ - children, -}: { - children: React_2.ReactChild; -}) => JSX.Element; - // @public export const useAsyncFilterValues: ( fn: ((partial: string) => Promise) | undefined, diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 48f12213e4..ff6f3d7fe1 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -101,8 +101,6 @@ export const DefaultResultListItemComponent = ({ }; /** - * A higher order function providing AnalyticsContext to the DefaultResultListItemComponent. - * * @public */ const HigherOrderDefaultResultListItem = ( diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index 54024d546a..46a2dde305 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -65,8 +65,6 @@ export const SearchResultComponent = ({ children }: SearchResultProps) => { }; /** - * A higher order function providing AnalyticsContext to the SearchResultComponent. - * * @public */ const HigherOrderSearchResult = (props: SearchResultProps) => { diff --git a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index 8ca7373298..f900e45d45 100644 --- a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -20,8 +20,6 @@ import { useSearch } from '../../context'; /** * Capture search event on term change. - * - * @public */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { const analytics = useAnalytics(); diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 155a4c3b12..263ca4ad59 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -19,5 +19,4 @@ export * from './SearchFilter'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchBar'; -export * from './SearchTracker'; export * from './DefaultResultListItem'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 9e418dd08e..85d4b783e5 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -99,7 +99,7 @@ export const SearchBarBase: ({ inputProps: defaultInputProps, endAdornment: defaultEndAdornment, ...props -}: SearchBarBaseProps) => JSX.Element; +}: SearchBarBaseProps_2) => JSX.Element; // @public @deprecated export type SearchBarBaseProps = Omit & { diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index 587478ed46..a854869976 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -14,36 +14,20 @@ * limitations under the License. */ -import React, { - ChangeEvent, - KeyboardEvent, - useState, - useEffect, - useCallback, -} from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { - InputBase, - InputBaseProps, - InputAdornment, - IconButton, -} from '@material-ui/core'; -import SearchIcon from '@material-ui/icons/Search'; -import ClearButton from '@material-ui/icons/Clear'; +import React, { useCallback } from 'react'; + +import { InputBaseProps } from '@material-ui/core'; import { - SearchContextProvider, + SearchBarBase as RealSearchBarBase, useSearch, - useSearchContextCheck, - TrackSearch, } from '@backstage/plugin-search-react'; /** * Props for {@link SearchBarBase}. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchBarBaseProps = Omit & { debounceTime?: number; @@ -59,102 +43,15 @@ export type SearchBarBaseProps = Omit & { * Recommended if you don't use Search Provider or Search Context. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ -export const SearchBarBase = ({ - onChange, - onKeyDown, - onSubmit, - debounceTime = 200, - clearButton = true, - fullWidth = true, - value: defaultValue, - inputProps: defaultInputProps = {}, - endAdornment: defaultEndAdornment, - ...props -}: SearchBarBaseProps) => { - const configApi = useApi(configApiRef); - const [value, setValue] = useState(defaultValue as string); - const hasSearchContext = useSearchContextCheck(); - - useEffect(() => { - setValue(prevValue => - prevValue !== defaultValue ? (defaultValue as string) : prevValue, - ); - }, [defaultValue]); - - useDebounce(() => onChange(value), debounceTime, [value]); - - const handleChange = useCallback( - (e: ChangeEvent) => { - setValue(e.target.value); - }, - [setValue], - ); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (onKeyDown) onKeyDown(e); - if (onSubmit && e.key === 'Enter') { - onSubmit(); - } - }, - [onKeyDown, onSubmit], - ); - - const handleClear = useCallback(() => { - onChange(''); - }, [onChange]); - - const placeholder = `Search in ${ - configApi.getOptionalString('app.title') || 'Backstage' - }`; - - const startAdornment = ( - - - - - - ); - - const endAdornment = ( - - - - - - ); - - const searchBar = ( - - - - ); - - return hasSearchContext ? ( - searchBar - ) : ( - {searchBar} - ); -}; +export const SearchBarBase = RealSearchBarBase; /** * Props for {@link SearchBar}. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchBarProps = Partial; @@ -162,7 +59,7 @@ export type SearchBarProps = Partial; * Recommended search bar when you use the Search Provider or Search Context. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index e7982ca5c4..e58bbd5c14 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -25,7 +25,7 @@ import { /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchFilterComponentProps = { className?: string; @@ -48,7 +48,7 @@ export type SearchFilterComponentProps = { /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchFilterWrapperProps = SearchFilterComponentProps & { component: (props: SearchFilterComponentProps) => ReactElement; @@ -57,7 +57,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ const SearchFilter = ({ component: Element, @@ -65,7 +65,7 @@ const SearchFilter = ({ }: SearchFilterWrapperProps) => ; /** - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ SearchFilter.Checkbox = ( props: Omit & @@ -73,7 +73,7 @@ SearchFilter.Checkbox = ( ) => ; /** - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ SearchFilter.Select = ( props: Omit & @@ -86,7 +86,7 @@ SearchFilter.Select = ( * which returns values may be provided instead. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index f99b6cf29f..287e485dd1 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -31,7 +31,7 @@ const useStyles = makeStyles(theme => ({ /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const SearchResultPager = () => { const { fetchNextPage, fetchPreviousPage } = useSearch(); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 15e43fb02d..619dafabd4 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -256,6 +256,7 @@ const NO_WARNING_PACKAGES = [ 'plugins/scaffolder-backend-module-rails', 'plugins/scaffolder-backend-module-yeoman', 'plugins/scaffolder-common', + 'plugins/search', 'plugins/search-backend', 'plugins/search-backend-node', 'plugins/search-backend-module-elasticsearch', From 7c26e81120cd6918116496f30664e0cc87d6d6ac Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 10:33:29 +0200 Subject: [PATCH 41/54] add some more docs to the template body type as well as optional composed_of Signed-off-by: Emma Indal --- .../api-report.md | 3 ++- .../src/engines/ElasticSearchSearchEngine.ts | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index a6be34dcb1..dfbb7e493f 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -146,7 +146,8 @@ export type ElasticSearchCustomIndexTemplate = { // @public export type ElasticSearchCustomIndexTemplateBody = { index_patterns: string[]; - template: Record; + composed_of?: string[]; + template?: Record; }; // @public (undocumented) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index a4b3be6eec..01bffdfff2 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -50,10 +50,21 @@ export type ElasticSearchCustomIndexTemplate = { * @public */ export type ElasticSearchCustomIndexTemplateBody = { + /** + * Array of wildcard (*) expressions used to match the names of data streams and indices during creation. + */ index_patterns: string[]; - // See available properties of template - // https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body - template: Record; + /** + * An ordered list of component template names. + * Component templates are merged in the order specified, + * meaning that the last component template specified has the highest precedence. + */ + composed_of?: string[]; + /** + * See available properties of template + * https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body + */ + template?: Record; }; /** From 935fa5d5d14a182aa150cf3e5acf6cb81303f7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 9 Jun 2022 16:41:12 +0200 Subject: [PATCH 42/54] remove some yarn.lock impurity in master by adjusting msw dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/vault-backend/package.json | 2 +- plugins/vault/dev/index.tsx | 67 ++++++++++++++++++- plugins/vault/package.json | 2 +- plugins/vault/src/api.test.ts | 4 +- .../EntityVaultTable.test.tsx | 6 +- 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/plugins/vault-backend/package.json b/plugins/vault-backend/package.json index 15e5be42a8..3ec616aa12 100644 --- a/plugins/vault-backend/package.json +++ b/plugins/vault-backend/package.json @@ -53,7 +53,7 @@ "@backstage/cli": "^0.17.2-next.1", "@types/compression": "^1.7.2", "@types/supertest": "^2.0.8", - "msw": "^0.35.0", + "msw": "^0.42.0", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/vault/dev/index.tsx b/plugins/vault/dev/index.tsx index fd89e65889..f5056cd057 100644 --- a/plugins/vault/dev/index.tsx +++ b/plugins/vault/dev/index.tsx @@ -13,7 +13,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Entity } from '@backstage/catalog-model'; +import { Content, Header, HeaderLabel, Page } from '@backstage/core-components'; import { createDevApp } from '@backstage/dev-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { TestApiProvider } from '@backstage/test-utils'; +import { Box, Typography } from '@material-ui/core'; +import SomeIcon from '@material-ui/icons/Storage'; +import React from 'react'; +import { VaultApi, vaultApiRef } from '../src/api'; +import { EntityVaultCard } from '../src/components/EntityVaultCard'; +import { EntityVaultTable } from '../src/components/EntityVaultTable'; +import { VAULT_SECRET_PATH_ANNOTATION } from '../src/constants'; import { vaultPlugin } from '../src/plugin'; -createDevApp().registerPlugin(vaultPlugin).render(); +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + annotations: { [VAULT_SECRET_PATH_ANNOTATION]: 'a/b' }, + }, + spec: { + type: 'service', + }, +}; + +const mockedApi: VaultApi = { + async listSecrets() { + return [ + { + name: 'a::b', + editUrl: 'https://example.com', + showUrl: 'https://example.com', + }, + { + name: 'c::d', + editUrl: 'https://example.com', + showUrl: 'https://example.com', + }, + ]; + }, +}; + +createDevApp() + .registerPlugin(vaultPlugin) + .addPage({ + element: ( + + + +
+ +
+ + As a card + + + As a table + + +
+
+
+ ), + title: 'Vault', + icon: SomeIcon, + }) + .render(); diff --git a/plugins/vault/package.json b/plugins/vault/package.json index 05b8da1df4..35f441e65e 100644 --- a/plugins/vault/package.json +++ b/plugins/vault/package.json @@ -58,7 +58,7 @@ "@types/jest": "*", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.35.0" + "msw": "^0.42.0" }, "files": [ "dist" diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 3452cbad38..70c6a553df 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -44,9 +44,9 @@ describe('api', () => { server.use( rest.get(`${mockBaseUrl}/v1/secrets/:path`, (req, res, ctx) => { const { path } = req.params; - if (path === 'test%2Fsuccess') { + if (path === 'test/success') { return res(ctx.json(mockSecretsResult)); - } else if (path === 'test%2Ferror') { + } else if (path === 'test/error') { return res(ctx.json([])); } return res(ctx.status(400)); diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 3538d2e0ae..f2b181562a 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -27,7 +27,7 @@ import { ApiProvider, UrlPatternDiscovery } from '@backstage/core-app-api'; import { VaultSecret, vaultApiRef, VaultClient } from '../../api'; import { rest } from 'msw'; -describe('EntityVautTable', () => { +describe('EntityVaultTable', () => { const server = setupServer(); setupRequestMockHandlers(server); let apis: TestApiRegistry; @@ -85,9 +85,9 @@ describe('EntityVautTable', () => { server.use( rest.get(`${mockBaseUrl}/v1/secrets/:path`, (req, res, ctx) => { const { path } = req.params; - if (path === 'test%2Fsuccess') { + if (path === 'test/success') { return res(ctx.json(mockSecretsResult)); - } else if (path === 'test%2Ferror') { + } else if (path === 'test/error') { return res(ctx.json([])); } return res(ctx.status(400)); From 2fa88266783ae8da838ddb83c81899ef98150ad2 Mon Sep 17 00:00:00 2001 From: Pit Wegner Date: Fri, 10 Jun 2022 02:21:53 -0700 Subject: [PATCH 43/54] add component dependency in text and diagram Signed-off-by: pitwegner --- .../software-catalog/software-model-core-entities.drawio.svg | 3 ++- .../software-catalog/software-model-entities.drawio.svg | 3 ++- docs/features/software-catalog/system-model.md | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) mode change 100644 => 100755 docs/assets/software-catalog/software-model-core-entities.drawio.svg mode change 100644 => 100755 docs/assets/software-catalog/software-model-entities.drawio.svg diff --git a/docs/assets/software-catalog/software-model-core-entities.drawio.svg b/docs/assets/software-catalog/software-model-core-entities.drawio.svg old mode 100644 new mode 100755 index 2260e5502e..1a8f63f9b2 --- a/docs/assets/software-catalog/software-model-core-entities.drawio.svg +++ b/docs/assets/software-catalog/software-model-core-entities.drawio.svg @@ -1,3 +1,4 @@ + -
dependsOn
dependsOn
Resource
(e.g. SQL Database, S3 bucket, ...)
Resource...
consumesAPI
consumesAPI
API
(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
API...
providesAPI
providesAPI
Component
(e.g. backend service, data pipeline ...)
Component...
Viewer does not support full SVG 1.1
\ No newline at end of file +
dependsOn
dependsOn
Resource
(e.g. SQL Database, S3 bucket, ...)
Resource...
consumesAPI
consumesAPI
API
(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
API...
providesAPI
providesAPI
Component
(e.g. backend service, data pipeline ...)
Component...
dependsOn
dependsOn
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/assets/software-catalog/software-model-entities.drawio.svg b/docs/assets/software-catalog/software-model-entities.drawio.svg old mode 100644 new mode 100755 index 7b8b88f224..e14cc93e62 --- a/docs/assets/software-catalog/software-model-entities.drawio.svg +++ b/docs/assets/software-catalog/software-model-entities.drawio.svg @@ -1,3 +1,4 @@ + -
Domain
Domain
partOf
partOf
System
System
dependsOn
dependsOn
partOf
partOf
Resource
(e.g. SQL Database, S3 bucket, ...)
Resource...
consumesAPI
consumesAPI
API
(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
API...
providesAPI
providesAPI
partOf
partOf
Component
(e.g. backend service, data pipeline ...)
Component...
partOf
partOf
Viewer does not support full SVG 1.1
\ No newline at end of file +
Domain
Domain
partOf
partOf
System
System
dependsOn
dependsOn
partOf
partOf
Resource
(e.g. SQL Database, S3 bucket, ...)
Resource...
consumesAPI
consumesAPI
API
(e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
API...
providesAPI
providesAPI
partOf
partOf
Component
(e.g. backend service, data pipeline ...)
Component...
partOf
partOf
dependsOn
dependsOn
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index eedb94d02e..8a7e0cd30a 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -33,8 +33,8 @@ tracked in source control, or use some existing open source or commercial software. A component can implement APIs for other components to consume. In turn it might -depend on APIs implemented by other components, or resources that are attached -to it at runtime. +consume APIs implemented by other components, or directly depend on components or +resources that are attached to it at runtime. ### API From effb3b27a40931ea317b0fe2f426d6e4799563d7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 10 Jun 2022 11:05:41 +0200 Subject: [PATCH 44/54] Further review feedback. Signed-off-by: Eric Peterson --- plugins/search-react/api-report.md | 37 +------------------ .../components/SearchBar/SearchBar.test.tsx | 8 ++-- .../src/components/SearchBar/SearchBar.tsx | 14 ++++++- .../src/components/SearchFilter/index.ts | 1 - plugins/search/api-report.md | 12 +++--- .../search/src/components/Filters/Filters.tsx | 8 ++++ .../src/components/Filters/FiltersButton.tsx | 3 ++ 7 files changed, 35 insertions(+), 48 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index ce1fa53bb4..f9bb993fd6 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -27,7 +27,7 @@ export const AutocompleteFilter: ( // @public (undocumented) export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; -// @public +// @public (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, ) => JSX.Element; @@ -168,7 +168,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -// @public +// @public (undocumented) export const SearchResult: (props: SearchResultProps) => JSX.Element; // @public @@ -187,39 +187,6 @@ export type SearchResultProps = { // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; -// @public -export const useAsyncFilterValues: ( - fn: ((partial: string) => Promise) | undefined, - inputValue: string, - defaultValues?: string[], - debounce?: number, -) => - | { - loading: boolean; - error?: undefined; - value?: undefined; - } - | { - loading: false; - error: Error; - value?: undefined; - } - | { - loading: true; - error?: Error | undefined; - value?: string[] | undefined; - } - | { - loading: boolean; - value: string[]; - }; - -// @public -export const useDefaultFilterValue: ( - name: string, - defaultValue?: string | string[] | null | undefined, -) => void; - // @public export const useSearch: () => SearchContextValue; diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 4e2c4d7a5c..d65278f7ec 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -317,8 +317,8 @@ describe('SearchBar', () => { expect(analyticsApiSpy.getEvents()[0]).toEqual({ action: 'search', context: { - extension: 'App', - pluginId: 'root', + extension: 'SearchBar', + pluginId: 'search', routeRef: 'unknown', searchTypes: 'software-catalog,techdocs', }, @@ -340,8 +340,8 @@ describe('SearchBar', () => { expect(analyticsApiSpy.getEvents()[1]).toEqual({ action: 'search', context: { - extension: 'App', - pluginId: 'root', + extension: 'SearchBar', + pluginId: 'search', routeRef: 'unknown', searchTypes: 'software-catalog,techdocs', }, diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index b3c8ec9fa5..d3d25455ce 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -31,7 +31,11 @@ import { import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { + AnalyticsContext, + configApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { SearchContextProvider, @@ -175,5 +179,11 @@ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { [onChange, setTerm], ); - return ; + return ( + + + + ); }; diff --git a/plugins/search-react/src/components/SearchFilter/index.ts b/plugins/search-react/src/components/SearchFilter/index.ts index e159cff93d..9f92c7c8e2 100644 --- a/plugins/search-react/src/components/SearchFilter/index.ts +++ b/plugins/search-react/src/components/SearchFilter/index.ts @@ -16,7 +16,6 @@ export { CheckboxFilter, SearchFilter, SelectFilter } from './SearchFilter'; export { AutocompleteFilter } from './SearchFilter.Autocomplete'; -export { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; export type { SearchFilterComponentProps, SearchFilterWrapperProps, diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 85d4b783e5..800fa7e7fc 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -22,13 +22,13 @@ export const DefaultResultListItem: ( props: DefaultResultListItemProps, ) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export type FilterOptions = { kind: Array; lifecycle: Array; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const Filters: ({ filters, filterOptions, @@ -37,19 +37,19 @@ export const Filters: ({ updateChecked, }: FiltersProps) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export const FiltersButton: ({ numberOfSelectedFilters, handleToggleFilters, }: FiltersButtonProps) => JSX.Element; -// @public +// @public @deprecated export type FiltersButtonProps = { numberOfSelectedFilters: number; handleToggleFilters: () => void; }; -// @public +// @public @deprecated export type FiltersProps = { filters: FiltersState; filterOptions: FilterOptions; @@ -58,7 +58,7 @@ export type FiltersProps = { updateChecked: (filter: string) => void; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type FiltersState = { selected: string; checked: Array; diff --git a/plugins/search/src/components/Filters/Filters.tsx b/plugins/search/src/components/Filters/Filters.tsx index 9bbe90d32f..9170961e18 100644 --- a/plugins/search/src/components/Filters/Filters.tsx +++ b/plugins/search/src/components/Filters/Filters.tsx @@ -46,6 +46,8 @@ const useStyles = makeStyles(theme => ({ /** * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FiltersState = { selected: string; @@ -54,6 +56,8 @@ export type FiltersState = { /** * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FilterOptions = { kind: Array; @@ -64,6 +68,8 @@ export type FilterOptions = { * Props for {@link Filters}. * * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FiltersProps = { filters: FiltersState; @@ -75,6 +81,8 @@ export type FiltersProps = { /** * @public + * @deprecated This component will be removed in a future release. Use + * `SearchFilter` from `@backstage/plugin-search-react` instead. */ export const Filters = ({ filters, diff --git a/plugins/search/src/components/Filters/FiltersButton.tsx b/plugins/search/src/components/Filters/FiltersButton.tsx index 8d896bbf30..890c787689 100644 --- a/plugins/search/src/components/Filters/FiltersButton.tsx +++ b/plugins/search/src/components/Filters/FiltersButton.tsx @@ -32,6 +32,8 @@ const useStyles = makeStyles(theme => ({ * Props for {@link FiltersButton}. * * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FiltersButtonProps = { numberOfSelectedFilters: number; @@ -40,6 +42,7 @@ export type FiltersButtonProps = { /** * @public + * @deprecated See `SearchFilter` in `@backstage/plugin-search-react` instead. */ export const FiltersButton = ({ numberOfSelectedFilters, From ddce23d080673bb9d5b715e3467e29bb2586e2b4 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 11:38:46 +0200 Subject: [PATCH 45/54] prefix changeset with search-* Signed-off-by: Emma Indal --- .changeset/{wet-icons-shout.md => search-wet-icons-shout.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changeset/{wet-icons-shout.md => search-wet-icons-shout.md} (100%) diff --git a/.changeset/wet-icons-shout.md b/.changeset/search-wet-icons-shout.md similarity index 100% rename from .changeset/wet-icons-shout.md rename to .changeset/search-wet-icons-shout.md From a1d5de54124697b7d3f84f15e78f0c50134ca08c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 10 Jun 2022 10:14:45 +0000 Subject: [PATCH 46/54] chore(deps): update dependency @types/react-syntax-highlighter to v15.5.2 Signed-off-by: Renovate Bot --- yarn.lock | 89 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 73b604becb..92bad383fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4340,6 +4340,14 @@ resolved "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz#155ef21065427901994e765da8a0ba0eaae8b8bd" integrity sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw== +"@mswjs/cookies@^0.1.6": + version "0.1.7" + resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.1.7.tgz#d334081b2c51057a61c1dd7b76ca3cac02251651" + integrity sha512-bDg1ReMBx+PYDB4Pk7y1Q07Zz1iKIEUWQpkEXiA2lEWg9gvOZ8UBmGXilCEUvyYoRFlmr/9iXTRR69TrgSwX/Q== + dependencies: + "@types/set-cookie-parser" "^2.4.0" + set-cookie-parser "^2.4.6" + "@mswjs/cookies@^0.2.0": version "0.2.0" resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23" @@ -4348,6 +4356,18 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" +"@mswjs/interceptors@^0.12.6": + version "0.12.7" + resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" + integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg== + dependencies: + "@open-draft/until" "^1.0.3" + "@xmldom/xmldom" "^0.7.2" + debug "^4.3.2" + headers-utils "^3.0.2" + outvariant "^1.2.0" + strict-event-emitter "^0.2.0" + "@mswjs/interceptors@^0.15.1": version "0.15.3" resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.15.3.tgz#bcd17b5d7558d4f598007a4bb383b42dc9264f8d" @@ -6239,6 +6259,14 @@ resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7" integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w== +"@types/inquirer@^7.3.3": + version "7.3.3" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" + integrity sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/inquirer@^8.1.3": version "8.2.1" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.1.tgz#28a139be3105a1175e205537e8ac10830e38dbf4" @@ -6315,7 +6343,7 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== -"@types/js-levenshtein@^1.1.1": +"@types/js-levenshtein@^1.1.0", "@types/js-levenshtein@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== @@ -6680,9 +6708,9 @@ "@types/react" "*" "@types/react-syntax-highlighter@^15.0.0": - version "15.5.1" - resolved "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.1.tgz#0c459cdd94a882df05778e93a1514430cc641043" - integrity sha512-+yD6D8y21JqLf89cRFEyRfptVMqo2ROHyAlysRvFwT28gT5gDo3KOiXHwGilHcq9y/OKTjlWK0f/hZUicrBFPQ== + version "15.5.2" + resolved "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.2.tgz#b3450851c11b8526a27edf51d00769b69d8fbf20" + integrity sha512-cJJvwU8lQv/efGSo/LmPoaOqWi/B0AG4CNKKCn7HPUL25SqiPn1Vl+fV1JiUigJv97ruTZ8mo08+b8/0zoYufA== dependencies: "@types/react" "*" @@ -7409,7 +7437,7 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.5": +"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.2", "@xmldom/xmldom@^0.7.5": version "0.7.5" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== @@ -10127,7 +10155,7 @@ cookie@0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== -cookie@0.4.2, cookie@^0.4.2, cookie@~0.4.1: +cookie@0.4.2, cookie@^0.4.1, cookie@^0.4.2, cookie@~0.4.1: version "0.4.2" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== @@ -13988,6 +14016,11 @@ graphql-ws@^5.4.1: resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.8.2.tgz#800184b1addb20b3010dc06cb70877703a5fff20" integrity sha512-hYo8kTGzxePFJtMGC7Y4cbypwifMphIJJ7n4TDcVUAfviRwQBnmZAbfZlC+XFwWDUaR7raEDQPxWctpccmE0JQ== +graphql@^15.5.1: + version "15.8.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + graphql@^16.0.0, graphql@^16.3.0: version "16.5.0" resolved "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz#41b5c1182eaac7f3d47164fb247f61e4dfb69c85" @@ -14235,6 +14268,11 @@ headers-polyfill@^3.0.4: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.7.tgz#725c4f591e6748f46b036197eae102c92b959ff4" integrity sha512-JoLCAdCEab58+2/yEmSnOlficyHFpIl0XJqwu3l+Unkm1gXpFUYsThz6Yha3D6tNhocWkCPfyW0YVIGWFqTi7w== +headers-utils@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-3.0.2.tgz#dfc65feae4b0e34357308aefbcafa99c895e59ef" + integrity sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ== + helmet@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz#e98a5d4bf89ab8119c856018a3bcc82addadcd47" @@ -14826,7 +14864,7 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.0.0, inquirer@^8.2.0: +inquirer@^8.0.0, inquirer@^8.1.1, inquirer@^8.2.0: version "8.2.4" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== @@ -18662,6 +18700,32 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^0.35.0: + version "0.35.0" + resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38" + integrity sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w== + dependencies: + "@mswjs/cookies" "^0.1.6" + "@mswjs/interceptors" "^0.12.6" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/inquirer" "^7.3.3" + "@types/js-levenshtein" "^1.1.0" + chalk "^4.1.1" + chokidar "^3.4.2" + cookie "^0.4.1" + graphql "^15.5.1" + headers-utils "^3.0.2" + inquirer "^8.1.1" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.1" + node-match-path "^0.6.3" + statuses "^2.0.0" + strict-event-emitter "^0.2.0" + type-fest "^1.2.2" + yargs "^17.0.1" + msw@^0.39.2: version "0.39.2" resolved "https://registry.npmjs.org/msw/-/msw-0.39.2.tgz#832e9274db62c43cb79854d5a69dce031c700de8" @@ -18973,6 +19037,11 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" +node-match-path@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.6.3.tgz#55dd8443d547f066937a0752dce462ea7dc27551" + integrity sha512-fB1reOHKLRZCJMAka28hIxCwQLxGmd7WewOCBDYKpyA1KXi68A7vaGgdZAPhY2E6SXoYt3KqYCCvXLJ+O0Fu/Q== + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -19612,7 +19681,7 @@ outdent@^0.5.0: resolved "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== -outvariant@^1.2.1, outvariant@^1.3.0: +outvariant@^1.2.0, outvariant@^1.2.1, outvariant@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz#c39723b1d2cba729c930b74bf962317a81b9b1c9" integrity sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ== @@ -22680,7 +22749,7 @@ rxjs@7.5.5, rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: dependencies: tslib "^2.1.0" -rxjs@^6.3.3, rxjs@^6.6.0, rxjs@^6.6.3: +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -26356,7 +26425,7 @@ yargs@^17.0.0, yargs@^17.1.1, yargs@^17.2.1: y18n "^5.0.5" yargs-parser "^21.0.0" -yargs@^17.3.1: +yargs@^17.0.1, yargs@^17.3.1: version "17.5.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== From 58426f9c0fbea6ec5c028781431dc7b8d9f3f7e2 Mon Sep 17 00:00:00 2001 From: Joon Park Date: Fri, 10 Jun 2022 11:32:28 +0100 Subject: [PATCH 47/54] Create permission aggregation endpoints (#11695) * Create permission aggregation endpoints Signed-off-by: Joon Park * Spelling Signed-off-by: Joon Park * Refactor permission metadata aggregation into one endpoint Signed-off-by: Joe Porpeglia * Change parameter field shape Signed-off-by: Joon Park Co-authored-by: Joe Porpeglia --- .changeset/giant-birds-wink.md | 7 +++ .github/vale/Vocab/Backstage/accept.txt | 1 + plugins/permission-node/api-report.md | 1 + .../createPermissionIntegrationRouter.test.ts | 49 ++++++++++++++++--- .../createPermissionIntegrationRouter.ts | 22 +++++++-- 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 .changeset/giant-birds-wink.md diff --git a/.changeset/giant-birds-wink.md b/.changeset/giant-birds-wink.md new file mode 100644 index 0000000000..c3dc2446b2 --- /dev/null +++ b/.changeset/giant-birds-wink.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Added a new endpoint for aggregating permission metadata from a plugin backend: `/.well-known/backstage/permissions/metadata` + +By default, the metadata endpoint will return information about the permission rules supported by the plugin. Plugin authors can also provide an optional `permissions` parameter to `createPermissionIntegrationRouter`. If provided, these `Permission` objects will be included in the metadata returned by this endpoint. The `permissions` parameter will eventually be required in a future breaking change. diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 7631640199..198a16e566 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -275,6 +275,7 @@ scrollbar seb semlas semver +serializable Serverless shoutout siloed diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index b2e4ff45b8..702d363137 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -112,6 +112,7 @@ export const createPermissionIntegrationRouter: < TResource, >(options: { resourceType: TResourceType; + permissions?: Permission[] | undefined; rules: PermissionRule, unknown[]>[]; getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>; }) => express.Router; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 65c43c7f30..ac4005c487 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { + AuthorizeResult, + createPermission, + Permission, +} from '@backstage/plugin-permission-common'; import express, { Express, Router } from 'express'; import request, { Response } from 'supertest'; import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter'; @@ -26,22 +30,25 @@ const mockGetResources: jest.MockedFunction< resourceRefs.map(resourceRef => ({ id: resourceRef })), ); +const testPermission: Permission = createPermission({ + name: 'test.permission', + attributes: {}, +}); + const testRule1 = createPermissionRule({ name: 'test-rule-1', description: 'Test rule 1', resourceType: 'test-resource', - apply: jest.fn( - (_resource: any, _firstParam: string, _secondParam: number) => true, - ), - toQuery: jest.fn(), + apply: (_resource: any, _firstParam: string, _secondParam: number) => true, + toQuery: (_firstParam: string, _secondParam: number) => ({}), }); const testRule2 = createPermissionRule({ name: 'test-rule-2', description: 'Test rule 2', resourceType: 'test-resource', - apply: jest.fn((_resource: any, _firstParam: object) => false), - toQuery: jest.fn(), + apply: (_resource: any, _firstParam: object) => false, + toQuery: (_firstParam: object) => ({}), }); describe('createPermissionIntegrationRouter', () => { @@ -51,6 +58,7 @@ describe('createPermissionIntegrationRouter', () => { beforeAll(() => { router = createPermissionIntegrationRouter({ resourceType: 'test-resource', + permissions: [testPermission], getResources: mockGetResources, rules: [testRule1, testRule2], }); @@ -501,4 +509,31 @@ describe('createPermissionIntegrationRouter', () => { expect(response.error && response.error.text).toMatch(/invalid/i); }); }); + + describe('GET /.well-known/backstage/permissions/metadata', () => { + it('returns a list of permissions and rules used by a given backend', async () => { + const response = await request(app).get( + '/.well-known/backstage/permissions/metadata', + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + permissions: [testPermission], + rules: [ + { + name: testRule1.name, + description: testRule1.description, + resourceType: testRule1.resourceType, + parameters: { count: 2 }, + }, + { + name: testRule2.name, + description: testRule2.description, + resourceType: testRule2.resourceType, + parameters: { count: 1 }, + }, + ], + }); + }); + }); }); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 4bdb4397dd..28abc2f48d 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -23,6 +23,7 @@ import { AuthorizeResult, DefinitivePolicyDecision, IdentifiedPermissionMessage, + Permission, PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; @@ -174,6 +175,7 @@ export const createPermissionIntegrationRouter = < TResource, >(options: { resourceType: TResourceType; + permissions?: Array; // Do not infer value of TResourceType from supplied rules. // instead only consider the resourceType parameter, and // consider any rules whose resource type does not match @@ -183,8 +185,22 @@ export const createPermissionIntegrationRouter = < resourceRefs: string[], ) => Promise>; }): express.Router => { - const { resourceType, rules, getResources } = options; + const { resourceType, permissions, rules, getResources } = options; const router = Router(); + router.use(express.json()); + + router.get('/.well-known/backstage/permissions/metadata', (_, res) => { + const serializableRules = rules.map(rule => ({ + name: rule.name, + description: rule.description, + resourceType: rule.resourceType, + parameters: { + count: rule.toQuery.length, + }, + })); + + return res.json({ permissions, rules: serializableRules }); + }); const getRule = createGetRule(rules); @@ -202,8 +218,6 @@ export const createPermissionIntegrationRouter = < } }; - router.use(express.json()); - router.post( '/.well-known/backstage/permissions/apply-conditions', async (req, res: Response) => { @@ -227,7 +241,7 @@ export const createPermissionIntegrationRouter = < return acc; }, {} as Record); - return res.status(200).json({ + return res.json({ items: body.items.map(request => ({ id: request.id, result: applyConditions( From 941060a420abac1dcc3664e1b5bc5713a7d99bfd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 10 Jun 2022 11:07:18 +0000 Subject: [PATCH 48/54] fix(deps): update dependency @roadiehq/backstage-plugin-github-pull-requests to v2.1.3 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 92bad383fd..7dfacaf9f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5236,9 +5236,9 @@ zustand "3.6.9" "@roadiehq/backstage-plugin-github-pull-requests@^2.0.0": - version "2.1.2" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-2.1.2.tgz#1c8289679bb109a70c30b5ba090ea7d751553764" - integrity sha512-VXv1D1JhifX0B9nukxFa61MyXouPTXEYkeNwYB+Y3e0VfoPEGzAeE/Ub23rP8aVcYsbuyCA+6u/HQsrw6Vgpxw== + version "2.1.3" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-2.1.3.tgz#d37869c2ff5a51794d3d166e80260937e1d51e11" + integrity sha512-2/HVfFFbmSwRTweTYVh5TYEa7Rl2+TPI2nVPps9yse6DBYPpo26XBvhnRT8O8OdJIJ0D4IQe1mZxpeFiDPZQpA== dependencies: "@backstage/catalog-model" "^1.0.0" "@backstage/core-components" "^0.9.0" From 0b8c1b93e25e633701cdbb479db18fd27b86b6fd Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 13:31:46 +0200 Subject: [PATCH 49/54] setIndexTemplate to be directly responsible for calling putIndexTemplate Signed-off-by: Emma Indal --- .../engines/ElasticSearchSearchEngine.test.ts | 43 +++---------------- .../src/engines/ElasticSearchSearchEngine.ts | 24 +++-------- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index c1c8ed9c86..c114406d99 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -32,9 +32,6 @@ class ElasticSearchSearchEngineForTranslatorTests extends ElasticSearchSearchEng getTranslator() { return this.translator; } - getIndexTemplate() { - return this.indexTemplate; - } } const mock = new Mock(); @@ -93,16 +90,16 @@ describe('ElasticSearchSearchEngine', () => { }); describe('custom index template', () => { - it('should not have custom template set as default', async () => { - expect(inspectableSearchEngine.getIndexTemplate()).toBeUndefined(); - }); - it('should set custom index template', async () => { + const indexTemplateSpy = jest.fn().mockReturnValue(customIndexTemplate); + mock.add( + { method: 'PUT', path: '/_index_template/custom-index-template' }, + indexTemplateSpy, + ); await inspectableSearchEngine.setIndexTemplate(customIndexTemplate); - expect(inspectableSearchEngine.getIndexTemplate()).toMatchObject( - customIndexTemplate, - ); + expect(indexTemplateSpy).toHaveBeenCalled(); + expect(indexTemplateSpy).toHaveBeenCalledTimes(1); }); }); @@ -811,38 +808,12 @@ describe('ElasticSearchSearchEngine', () => { elasticSearchClient: client, }), ); - expect(indexerMock.on).toHaveBeenCalledWith( - 'finish', - expect.any(Function), - ); expect(indexerMock.on).toHaveBeenCalledWith( 'error', expect.any(Function), ); }); - describe('onFinish', () => { - let callback: Function; - - beforeEach(async () => { - mock.clearAll(); - await testSearchEngine.getIndexer('test-index'); - callback = indexerMock.on.mock.calls[1][1]; - }); - - it('should set provided index template on finish', async () => { - const indexTemplateSpy = jest.fn().mockReturnValue(customIndexTemplate); - mock.add( - { method: 'PUT', path: '/_index_template/custom-index-template' }, - indexTemplateSpy, - ); - - await callback(); - - expect(indexTemplateSpy).toHaveBeenCalled(); - }); - }); - describe('onError', () => { let errorHandler: Function; const error = new Error('some error'); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 01bffdfff2..d11d8a1f90 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -145,7 +145,6 @@ function isBlank(str: string) { export class ElasticSearchSearchEngine implements SearchEngine { private readonly elasticSearchClient: Client; private readonly highlightOptions: ElasticSearchHighlightConfig; - protected indexTemplate?: ElasticSearchCustomIndexTemplate; constructor( private readonly elasticSearchClientOptions: ElasticSearchClientOptions, @@ -268,8 +267,13 @@ export class ElasticSearchSearchEngine implements SearchEngine { this.translator = translator; } - setIndexTemplate(template: ElasticSearchCustomIndexTemplate) { - this.indexTemplate = template; + async setIndexTemplate(template: ElasticSearchCustomIndexTemplate) { + try { + await this.elasticSearchClient.indices.putIndexTemplate(template); + this.logger.info('Custom index template set'); + } catch (error) { + this.logger.error(`Unable to set custom index template: ${error}`); + } } async getIndexer(type: string) { @@ -303,20 +307,6 @@ export class ElasticSearchSearchEngine implements SearchEngine { } }); - indexer.on('finish', async () => { - // Set custom index template if set - if (this.indexTemplate) { - try { - await this.elasticSearchClient.indices.putIndexTemplate( - this.indexTemplate, - ); - this.logger.info('Custom index template set'); - } catch (error) { - this.logger.error(`Unable to set custom index template: ${error}`); - } - } - }); - return indexer; } From 33bea6d4a15fccb174a1baaa3533b4e63d7a9873 Mon Sep 17 00:00:00 2001 From: Raffi Tamizian Date: Fri, 10 Jun 2022 12:32:31 +0100 Subject: [PATCH 50/54] Update docs link to current techdocs-cli Signed-off-by: Raffi Tamizian --- docs/features/techdocs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 48c469def7..761a247819 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -95,7 +95,7 @@ See [TechDocs Architecture](architecture.md) to get an overview of where the bel [techdocs/frontend-library]: https://github.com/backstage/backstage/blob/master/plugins/techdocs-react [techdocs/backend]: https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend [techdocs/container]: https://github.com/backstage/techdocs-container -[techdocs/cli]: https://github.com/backstage/techdocs-cli +[techdocs/cli]: https://github.com/backstage/backstage/blob/master/packages/techdocs-cli ## Get involved From d3edcd078476e460db7a0e97e901d58c4170fde2 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 13:41:56 +0200 Subject: [PATCH 51/54] add instructions for how to set a custom index template Signed-off-by: Emma Indal --- docs/features/search/search-engines.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index f1fd4bd62b..9bb8b8c343 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -102,6 +102,31 @@ import { Client } from '@elastic/elastic-search'; const client = searchEngine.newClient(options => new Client(options)); ``` +#### Set custom index template + +The elasticsearch engine gives you the ability to set a custom index template if needed. + +> Index templates define settings, mappings, and aliases that can be applied automatically to new indices. + +```typescript +// app/backend/src/plugins/search.ts +const searchEngine = await ElasticSearchSearchEngine.initialize({ + logger: env.logger, + config: env.config, +}); + +searchEngine.setIndexTemplate({ + name: '', + body: { + index_patterns: [''], + template: { + mappings: {}, + settings: {}, + }, + }, +}); +``` + ## Example configurations ### AWS From 539c7bb4f75b62938acb6315b2f1e0730380b5d4 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 10 Jun 2022 13:43:37 +0200 Subject: [PATCH 52/54] update api report Signed-off-by: Emma Indal --- plugins/search-backend-module-elasticsearch/api-report.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index dfbb7e493f..04b7f96f93 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -224,13 +224,11 @@ export class ElasticSearchSearchEngine implements SearchEngine { }: ElasticSearchOptions): Promise; // (undocumented) getIndexer(type: string): Promise; - // (undocumented) - protected indexTemplate?: ElasticSearchCustomIndexTemplate; newClient(create: (options: ElasticSearchClientOptions) => T): T; // (undocumented) query(query: SearchQuery): Promise; // (undocumented) - setIndexTemplate(template: ElasticSearchCustomIndexTemplate): void; + setIndexTemplate(template: ElasticSearchCustomIndexTemplate): Promise; // (undocumented) setTranslator(translator: ElasticSearchQueryTranslator): void; // (undocumented) From e5cf887fc38e0fa332a62405c9201b4c2a3150c2 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 10 Jun 2022 12:18:57 +0000 Subject: [PATCH 53/54] fix(deps): update dependency @yarnpkg/parsers to v3.0.0-rc.9 Signed-off-by: Renovate Bot --- yarn.lock | 89 +++++++------------------------------------------------ 1 file changed, 10 insertions(+), 79 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7dfacaf9f1..a82b4d5dab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4340,14 +4340,6 @@ resolved "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz#155ef21065427901994e765da8a0ba0eaae8b8bd" integrity sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw== -"@mswjs/cookies@^0.1.6": - version "0.1.7" - resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.1.7.tgz#d334081b2c51057a61c1dd7b76ca3cac02251651" - integrity sha512-bDg1ReMBx+PYDB4Pk7y1Q07Zz1iKIEUWQpkEXiA2lEWg9gvOZ8UBmGXilCEUvyYoRFlmr/9iXTRR69TrgSwX/Q== - dependencies: - "@types/set-cookie-parser" "^2.4.0" - set-cookie-parser "^2.4.6" - "@mswjs/cookies@^0.2.0": version "0.2.0" resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23" @@ -4356,18 +4348,6 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" -"@mswjs/interceptors@^0.12.6": - version "0.12.7" - resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" - integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg== - dependencies: - "@open-draft/until" "^1.0.3" - "@xmldom/xmldom" "^0.7.2" - debug "^4.3.2" - headers-utils "^3.0.2" - outvariant "^1.2.0" - strict-event-emitter "^0.2.0" - "@mswjs/interceptors@^0.15.1": version "0.15.3" resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.15.3.tgz#bcd17b5d7558d4f598007a4bb383b42dc9264f8d" @@ -6259,14 +6239,6 @@ resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7" integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w== -"@types/inquirer@^7.3.3": - version "7.3.3" - resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" - integrity sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ== - dependencies: - "@types/through" "*" - rxjs "^6.4.0" - "@types/inquirer@^8.1.3": version "8.2.1" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.1.tgz#28a139be3105a1175e205537e8ac10830e38dbf4" @@ -6343,7 +6315,7 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== -"@types/js-levenshtein@^1.1.0", "@types/js-levenshtein@^1.1.1": +"@types/js-levenshtein@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== @@ -7437,7 +7409,7 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.2", "@xmldom/xmldom@^0.7.5": +"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.5": version "0.7.5" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== @@ -7463,9 +7435,9 @@ integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== "@yarnpkg/parsers@^3.0.0-rc.4": - version "3.0.0-rc.7" - resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.7.tgz#2be47b765b36518c1148e2d4e86c919be4f2101d" - integrity sha512-MTEcH+5vmkspbMD53S6eHEfpRHer3pXOl0vCBYNTPivsxmTU14As5yoKyJvGjM/KN7/Madzz+WoDUvYNs7an+A== + version "3.0.0-rc.9" + resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.9.tgz#2d284e4e0c79b1c4e410465e217fa303d98b7be3" + integrity sha512-JMBE+6OJoNN9AXBzZ72u22/t9M25K8KgUWZIjvk8CU/NsE/m946l8D7SqMhbi3ZaUfYa5QitqCqVWTTFtysGJQ== dependencies: js-yaml "^3.10.0" tslib "^1.13.0" @@ -10155,7 +10127,7 @@ cookie@0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== -cookie@0.4.2, cookie@^0.4.1, cookie@^0.4.2, cookie@~0.4.1: +cookie@0.4.2, cookie@^0.4.2, cookie@~0.4.1: version "0.4.2" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== @@ -14016,11 +13988,6 @@ graphql-ws@^5.4.1: resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.8.2.tgz#800184b1addb20b3010dc06cb70877703a5fff20" integrity sha512-hYo8kTGzxePFJtMGC7Y4cbypwifMphIJJ7n4TDcVUAfviRwQBnmZAbfZlC+XFwWDUaR7raEDQPxWctpccmE0JQ== -graphql@^15.5.1: - version "15.8.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" - integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== - graphql@^16.0.0, graphql@^16.3.0: version "16.5.0" resolved "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz#41b5c1182eaac7f3d47164fb247f61e4dfb69c85" @@ -14268,11 +14235,6 @@ headers-polyfill@^3.0.4: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.7.tgz#725c4f591e6748f46b036197eae102c92b959ff4" integrity sha512-JoLCAdCEab58+2/yEmSnOlficyHFpIl0XJqwu3l+Unkm1gXpFUYsThz6Yha3D6tNhocWkCPfyW0YVIGWFqTi7w== -headers-utils@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-3.0.2.tgz#dfc65feae4b0e34357308aefbcafa99c895e59ef" - integrity sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ== - helmet@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz#e98a5d4bf89ab8119c856018a3bcc82addadcd47" @@ -14864,7 +14826,7 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.0.0, inquirer@^8.1.1, inquirer@^8.2.0: +inquirer@^8.0.0, inquirer@^8.2.0: version "8.2.4" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== @@ -18700,32 +18662,6 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msw@^0.35.0: - version "0.35.0" - resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38" - integrity sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w== - dependencies: - "@mswjs/cookies" "^0.1.6" - "@mswjs/interceptors" "^0.12.6" - "@open-draft/until" "^1.0.3" - "@types/cookie" "^0.4.1" - "@types/inquirer" "^7.3.3" - "@types/js-levenshtein" "^1.1.0" - chalk "^4.1.1" - chokidar "^3.4.2" - cookie "^0.4.1" - graphql "^15.5.1" - headers-utils "^3.0.2" - inquirer "^8.1.1" - is-node-process "^1.0.1" - js-levenshtein "^1.1.6" - node-fetch "^2.6.1" - node-match-path "^0.6.3" - statuses "^2.0.0" - strict-event-emitter "^0.2.0" - type-fest "^1.2.2" - yargs "^17.0.1" - msw@^0.39.2: version "0.39.2" resolved "https://registry.npmjs.org/msw/-/msw-0.39.2.tgz#832e9274db62c43cb79854d5a69dce031c700de8" @@ -19037,11 +18973,6 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-match-path@^0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.6.3.tgz#55dd8443d547f066937a0752dce462ea7dc27551" - integrity sha512-fB1reOHKLRZCJMAka28hIxCwQLxGmd7WewOCBDYKpyA1KXi68A7vaGgdZAPhY2E6SXoYt3KqYCCvXLJ+O0Fu/Q== - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -19681,7 +19612,7 @@ outdent@^0.5.0: resolved "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== -outvariant@^1.2.0, outvariant@^1.2.1, outvariant@^1.3.0: +outvariant@^1.2.1, outvariant@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz#c39723b1d2cba729c930b74bf962317a81b9b1c9" integrity sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ== @@ -22749,7 +22680,7 @@ rxjs@7.5.5, rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: dependencies: tslib "^2.1.0" -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3: +rxjs@^6.3.3, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -26425,7 +26356,7 @@ yargs@^17.0.0, yargs@^17.1.1, yargs@^17.2.1: y18n "^5.0.5" yargs-parser "^21.0.0" -yargs@^17.0.1, yargs@^17.3.1: +yargs@^17.3.1: version "17.5.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== From 0f20dbace9b290dc911ed66f2fbeeb9adaf53b70 Mon Sep 17 00:00:00 2001 From: "jean-philippe.blary" Date: Fri, 10 Jun 2022 14:28:24 +0200 Subject: [PATCH 54/54] docs: add OVHcloud as adopter Signed-off-by: jean-philippe.blary --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index f78ffb3a2c..0630ea9b4a 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -158,3 +158,4 @@ _If you're using Backstage in your organization, please try to add your company | [Meltwater](https://underthehood.meltwater.com) | [@spier](https://github.com/spier), [@remen](https://github.com/remen) | Improving developer experience by centralizing documentation and internal APIs. Goal: Foster InnerSource collaboration and speed up onboarding time in our 500+ people Product & Engineering org. | [Doctolib](https://doctolib.engineering/) | [@djiit](https://github.com/djiit) | Rails modularization effort awareness, tech organization discoverability. Improving the daily workflows and collaboration processes of our engineers. | [Twilio](https://www.twilio.com) | [Kyle Smith](https://github.com/knksmith57) | Developer portal, universal software catalog, and centralized taxonomy platform. +| [OVHcloud](https://www.ovhcloud.com/fr/) | [Jean-Philippe Blary](https://github.com/blaryjp), [Arnaud Bauer](mailto:arnaud.bauer@ovhcloud.com), [Flavien Chantelot](https://github.com/Dorn-) | We're providing Backstage to our collaborators to ease their daily jobs, and let them extends it using plugins.