From 221e951298b01bf9987085189291611185f3be03 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Wed, 24 Aug 2022 23:02:56 +0200 Subject: [PATCH 01/25] passing https.cert and https.key options to the webpack dev server Signed-off-by: Luka Siric --- .changeset/fast-paws-press.md | 6 ++++++ app-config.yaml | 7 ++++++- packages/cli/src/lib/bundler/server.ts | 12 +++++++++++- packages/core-app-api/config.d.ts | 20 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-paws-press.md diff --git a/.changeset/fast-paws-press.md b/.changeset/fast-paws-press.md new file mode 100644 index 0000000000..353a2f422c --- /dev/null +++ b/.changeset/fast-paws-press.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli': patch +'@backstage/core-app-api': patch +--- + +Added support for custom certificate for webpack dev server. diff --git a/app-config.yaml b/app-config.yaml index 32b53da236..4bbb272d00 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -2,12 +2,17 @@ app: title: Backstage Example App baseUrl: http://localhost:3000 googleAnalyticsTrackingId: # UA-000000-0 + # https: + # credentials: + # cert: + # $file '\path\to\the\certificate' + # key: + # $file '\path\to\the\private-key' #datadogRum: # clientToken: '123456789' # applicationId: qwerty # site: # datadoghq.eu default = datadoghq.com # env: # optional - support: url: https://github.com/backstage/backstage/issues # Used by common ErrorPage items: # Used by common SupportButton component diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index d573f80e27..3057bcb70b 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -60,7 +60,17 @@ export async function serveBundle(options: ServeOptions) { // See https://github.com/facebookincubator/create-react-app/issues/387. disableDotRule: true, }, - https: url.protocol === 'https:', + https: + url.protocol === 'https:' + ? { + cert: options.frontendConfig.getOptionalString( + 'app.https.credentials.cert', + ), + key: options.frontendConfig.getOptionalString( + 'app.https.credentials.key', + ), + } + : false, host, port, proxy: pkg.proxy, diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts index d88c818d11..e5047709d8 100644 --- a/packages/core-app-api/config.d.ts +++ b/packages/core-app-api/config.d.ts @@ -65,6 +65,26 @@ export interface Config { }>; }>; }; + /** + * Running the frontend app with https + */ + https?: { + /** + * Parent object containing certificate and the private key + */ + credentials?: { + /** + * Https Certificate private key. Can be loaded using $file + * @visibility frontend + */ + key?: string; + /** + * Https Certificate. Can be loaded using $file + * @visibility frontend + */ + cert?: string; + }; + }; }; /** From 37763a03840847c1a3c7a054d8e8ca32ef667d1e Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Fri, 26 Aug 2022 12:28:32 +0200 Subject: [PATCH 02/25] fixed comments from benjdlambert Signed-off-by: Luka Siric --- .changeset/fast-paws-press.md | 2 +- app-config.yaml | 6 +++--- packages/cli/src/lib/bundler/server.ts | 5 +++-- packages/core-app-api/config.d.ts | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.changeset/fast-paws-press.md b/.changeset/fast-paws-press.md index 353a2f422c..b9e1893963 100644 --- a/.changeset/fast-paws-press.md +++ b/.changeset/fast-paws-press.md @@ -1,6 +1,6 @@ --- '@backstage/cli': patch -'@backstage/core-app-api': patch +'@backstage/core-app-api': minor --- Added support for custom certificate for webpack dev server. diff --git a/app-config.yaml b/app-config.yaml index 4bbb272d00..fc7215ceb8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -3,11 +3,11 @@ app: baseUrl: http://localhost:3000 googleAnalyticsTrackingId: # UA-000000-0 # https: - # credentials: + # certificate: # cert: - # $file '\path\to\the\certificate' + # $file: '\path\to\the\certificate' # key: - # $file '\path\to\the\private-key' + # $file: '\path\to\the\private-key' #datadogRum: # clientToken: '123456789' # applicationId: qwerty diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 3057bcb70b..2e362cd050 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -40,6 +40,7 @@ export async function serveBundle(options: ServeOptions) { isDev: true, baseUrl: url, }); + const compiler = webpack(config); const server = new WebpackDevServer( @@ -64,10 +65,10 @@ export async function serveBundle(options: ServeOptions) { url.protocol === 'https:' ? { cert: options.frontendConfig.getOptionalString( - 'app.https.credentials.cert', + 'app.https.certificate.cert', ), key: options.frontendConfig.getOptionalString( - 'app.https.credentials.key', + 'app.https.certificate.key', ), } : false, diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts index e5047709d8..42b1fd9245 100644 --- a/packages/core-app-api/config.d.ts +++ b/packages/core-app-api/config.d.ts @@ -72,14 +72,14 @@ export interface Config { /** * Parent object containing certificate and the private key */ - credentials?: { + certificate?: { /** - * Https Certificate private key. Can be loaded using $file + * Https Certificate private key. Use $file to load in a file * @visibility frontend */ key?: string; /** - * Https Certificate. Can be loaded using $file + * Https Certificate. Use $file to load in a file * @visibility frontend */ cert?: string; From 08eb42afdcb4fe9caf8ce6525719aee17f0057ed Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Mon, 29 Aug 2022 12:30:48 +0200 Subject: [PATCH 03/25] added backend config to startFrontend. set cert key visibility to secret. Signed-off-by: Luka Siric --- packages/cli/src/lib/bundler/server.ts | 2 +- packages/cli/src/lib/bundler/types.ts | 2 ++ packages/cli/src/lib/config.ts | 9 +++++++++ packages/core-app-api/config.d.ts | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 2e362cd050..a05baf5355 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -67,7 +67,7 @@ export async function serveBundle(options: ServeOptions) { cert: options.frontendConfig.getOptionalString( 'app.https.certificate.cert', ), - key: options.frontendConfig.getOptionalString( + key: options.backendConfig.getOptionalString( 'app.https.certificate.key', ), } diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 4d6d2e5c9e..0d9d0f7af4 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -31,6 +31,8 @@ export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; frontendConfig: Config; frontendAppConfigs: AppConfig[]; + backendConfig: Config; + backendAppConfigs: AppConfig[]; }; export type BuildOptions = BundlingPathsOptions & { diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index e7ceeadf08..691c0ff83c 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -96,11 +96,20 @@ export async function loadCliConfig(options: Options) { }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); + const backendAppConfigs = schema.process(appConfigs, { + visibility: ['frontend', 'backend', 'secret'], + withFilteredKeys: options.withFilteredKeys, + withDeprecatedKeys: options.withDeprecatedKeys, + }); + const backendConfig = ConfigReader.fromConfigs(backendAppConfigs); + return { schema, appConfigs, frontendConfig, frontendAppConfigs, + backendAppConfigs, + backendConfig, }; } catch (error) { const maybeSchemaError = error as Error & { messages?: string[] }; diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts index 42b1fd9245..6b707987e5 100644 --- a/packages/core-app-api/config.d.ts +++ b/packages/core-app-api/config.d.ts @@ -75,7 +75,7 @@ export interface Config { certificate?: { /** * Https Certificate private key. Use $file to load in a file - * @visibility frontend + * @visibility secret */ key?: string; /** From eef0522644ed9d50fcc7fd69e7a712c2719b47ab Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 30 Aug 2022 16:33:47 +0200 Subject: [PATCH 04/25] resolved @Rugvip comments Signed-off-by: Luka Siric --- app-config.yaml | 6 ---- packages/cli/config.d.ts | 40 ++++++++++++++++++++++++++ packages/cli/src/lib/bundler/server.ts | 6 ++-- packages/cli/src/lib/bundler/types.ts | 1 - packages/cli/src/lib/config.ts | 7 +---- packages/core-app-api/config.d.ts | 20 ------------- 6 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 packages/cli/config.d.ts diff --git a/app-config.yaml b/app-config.yaml index fc7215ceb8..7c93d9cbd2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -2,12 +2,6 @@ app: title: Backstage Example App baseUrl: http://localhost:3000 googleAnalyticsTrackingId: # UA-000000-0 - # https: - # certificate: - # cert: - # $file: '\path\to\the\certificate' - # key: - # $file: '\path\to\the\private-key' #datadogRum: # clientToken: '123456789' # applicationId: qwerty diff --git a/packages/cli/config.d.ts b/packages/cli/config.d.ts new file mode 100644 index 0000000000..24cc60dac3 --- /dev/null +++ b/packages/cli/config.d.ts @@ -0,0 +1,40 @@ +/* + * 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 interface Config { + app: { + /** + * Running the frontend app with https + */ + https?: { + /** + * Parent object containing certificate and the private key + */ + certificate?: { + /** + * Https Certificate private key. Use $file to load in a file + * @visibility secret + */ + key: string; + /** + * Https Certificate. Use $file to load in a file + * @visibility secret + */ + cert: string; + }; + }; + }; +} diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index a05baf5355..d3f1e8da75 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -64,12 +64,10 @@ export async function serveBundle(options: ServeOptions) { https: url.protocol === 'https:' ? { - cert: options.frontendConfig.getOptionalString( + cert: options.backendConfig.getString( 'app.https.certificate.cert', ), - key: options.backendConfig.getOptionalString( - 'app.https.certificate.key', - ), + key: options.backendConfig.getString('app.https.certificate.key'), } : false, host, diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 0d9d0f7af4..68260735f9 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -32,7 +32,6 @@ export type ServeOptions = BundlingPathsOptions & { frontendConfig: Config; frontendAppConfigs: AppConfig[]; backendConfig: Config; - backendAppConfigs: AppConfig[]; }; export type BuildOptions = BundlingPathsOptions & { diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 691c0ff83c..994714a3ce 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -96,11 +96,7 @@ export async function loadCliConfig(options: Options) { }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); - const backendAppConfigs = schema.process(appConfigs, { - visibility: ['frontend', 'backend', 'secret'], - withFilteredKeys: options.withFilteredKeys, - withDeprecatedKeys: options.withDeprecatedKeys, - }); + const backendAppConfigs = schema.process(appConfigs); const backendConfig = ConfigReader.fromConfigs(backendAppConfigs); return { @@ -108,7 +104,6 @@ export async function loadCliConfig(options: Options) { appConfigs, frontendConfig, frontendAppConfigs, - backendAppConfigs, backendConfig, }; } catch (error) { diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts index 6b707987e5..d88c818d11 100644 --- a/packages/core-app-api/config.d.ts +++ b/packages/core-app-api/config.d.ts @@ -65,26 +65,6 @@ export interface Config { }>; }>; }; - /** - * Running the frontend app with https - */ - https?: { - /** - * Parent object containing certificate and the private key - */ - certificate?: { - /** - * Https Certificate private key. Use $file to load in a file - * @visibility secret - */ - key?: string; - /** - * Https Certificate. Use $file to load in a file - * @visibility frontend - */ - cert?: string; - }; - }; }; /** From 6ec5064c94fd60df14d35df5f0620add0dda122e Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Wed, 31 Aug 2022 12:27:18 +0200 Subject: [PATCH 05/25] resolved @benjdlambert comments Signed-off-by: Luka Siric --- .changeset/fast-paws-press.md | 1 - packages/cli/config.d.ts | 40 ----------------------------------- packages/cli/package.json | 23 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 41 deletions(-) delete mode 100644 packages/cli/config.d.ts diff --git a/.changeset/fast-paws-press.md b/.changeset/fast-paws-press.md index b9e1893963..8ddb0bc49e 100644 --- a/.changeset/fast-paws-press.md +++ b/.changeset/fast-paws-press.md @@ -1,6 +1,5 @@ --- '@backstage/cli': patch -'@backstage/core-app-api': minor --- Added support for custom certificate for webpack dev server. diff --git a/packages/cli/config.d.ts b/packages/cli/config.d.ts deleted file mode 100644 index 24cc60dac3..0000000000 --- a/packages/cli/config.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Config { - app: { - /** - * Running the frontend app with https - */ - https?: { - /** - * Parent object containing certificate and the private key - */ - certificate?: { - /** - * Https Certificate private key. Use $file to load in a file - * @visibility secret - */ - key: string; - /** - * Https Certificate. Use $file to load in a file - * @visibility secret - */ - cert: string; - }; - }; - }; -} diff --git a/packages/cli/package.json b/packages/cli/package.json index 27b540bff6..a4162c5e80 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -248,6 +248,29 @@ "description": "The port that the frontend should be bound to. Only used for local development." } } + }, + "https": { + "type": "object", + "description": "Running the frontend app with https", + "properties": { + "certificate": { + "type": "object", + "description": "Parent object containing certificate and the private key", + "required": ["key", "cert"], + "properties" : { + "key": { + "type" : "string", + "visibility": "secret", + "description": "Https Certificate private key. Use $file to load in a file" + }, + "cert": { + "type" : "string", + "visibility": "secret", + "description": "Https Certificate. Use $file to load in a file" + } + } + } + } } } } From c55bdf2ad9f34bba7751982e023e3eac55a09da2 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Wed, 31 Aug 2022 13:04:23 +0200 Subject: [PATCH 06/25] run prettier Signed-off-by: Luka Siric --- packages/cli/package.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index a4162c5e80..b978833d29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -256,15 +256,18 @@ "certificate": { "type": "object", "description": "Parent object containing certificate and the private key", - "required": ["key", "cert"], - "properties" : { + "required": [ + "key", + "cert" + ], + "properties": { "key": { - "type" : "string", + "type": "string", "visibility": "secret", "description": "Https Certificate private key. Use $file to load in a file" }, "cert": { - "type" : "string", + "type": "string", "visibility": "secret", "description": "Https Certificate. Use $file to load in a file" } From 3a446f905c7af629c42d54489b8f7689b764e665 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Wed, 31 Aug 2022 16:55:08 +0200 Subject: [PATCH 07/25] modified https setting description in package.json Signed-off-by: Luka Siric --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index b978833d29..01e92c89d9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -251,7 +251,7 @@ }, "https": { "type": "object", - "description": "Running the frontend app with https", + "description": "Only used for local development. The https object is passed to webpack in order to enable using https on localhost.", "properties": { "certificate": { "type": "object", From 68c269707749addfc7223e032c8651f7ecc88071 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 31 Aug 2022 22:00:35 +0200 Subject: [PATCH 08/25] cli: added new repo clean command Signed-off-by: Patrik Oldsberg --- .changeset/smart-squids-change.md | 5 +++ packages/cli/cli-report.md | 10 +++++ packages/cli/src/commands/index.ts | 5 +++ packages/cli/src/commands/repo/clean.ts | 57 +++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .changeset/smart-squids-change.md create mode 100644 packages/cli/src/commands/repo/clean.ts diff --git a/.changeset/smart-squids-change.md b/.changeset/smart-squids-change.md new file mode 100644 index 0000000000..51396114c9 --- /dev/null +++ b/.changeset/smart-squids-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a new `backstage-cli repo clean` command that cleans the repo root and runs the clean script in all packages. diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index dabab76850..14e548eeaf 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -415,6 +415,7 @@ Options: Commands: build [options] lint [options] + clean help [command] ``` @@ -429,6 +430,15 @@ Options: -h, --help ``` +### `backstage-cli repo clean` + +``` +Usage: backstage-cli repo clean [options] + +Options: + -h, --help +``` + ### `backstage-cli repo lint` ``` diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index def16900fa..9c543f8853 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -60,6 +60,11 @@ export function registerRepoCommand(program: Command) { .option('--fix', 'Attempt to automatically fix violations') .action(lazy(() => import('./repo/lint').then(m => m.command))); + command + .command('clean') + .description('Delete cache and output directories') + .action(lazy(() => import('./repo/clean').then(m => m.command))); + command .command('list-deprecations', { hidden: true }) .description('List deprecations. [EXPERIMENTAL]') diff --git a/packages/cli/src/commands/repo/clean.ts b/packages/cli/src/commands/repo/clean.ts new file mode 100644 index 0000000000..323746e089 --- /dev/null +++ b/packages/cli/src/commands/repo/clean.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. + */ + +import { execFile as execFileCb } from 'child_process'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { promisify } from 'util'; +import { PackageGraph } from '../../lib/monorepo'; +import { paths } from '../../lib/paths'; + +const execFile = promisify(execFileCb); + +export async function command(): Promise { + const packages = await PackageGraph.listTargetPackages(); + + await fs.remove(paths.resolveTargetRoot('dist')); + await fs.remove(paths.resolveTargetRoot('dist-types')); + await fs.remove(paths.resolveTargetRoot('coverage')); + + await Promise.all( + Array.from(Array(10), async () => { + while (packages.length > 0) { + const pkg = packages.pop()!; + const cleanScript = pkg.packageJson.scripts?.clean; + + if ( + cleanScript === 'backstage-cli clean' || + cleanScript === 'backstage-cli package clean' + ) { + await fs.remove(resolvePath(pkg.dir, 'dist')); + await fs.remove(resolvePath(pkg.dir, 'dist-types')); + await fs.remove(resolvePath(pkg.dir, 'coverage')); + } else if (cleanScript) { + const result = await execFile('yarn', ['run', 'clean'], { + cwd: pkg.dir, + shell: true, + }); + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + } + } + }), + ); +} From a578558180301d7a11746354c965fa00d0e57ca3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 31 Aug 2022 22:02:45 +0200 Subject: [PATCH 09/25] create-app: update to use new repo clean command Signed-off-by: Patrik Oldsberg --- .changeset/weak-camels-roll.md | 12 ++++++++++++ .../templates/default-app/package.json.hbs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/weak-camels-roll.md diff --git a/.changeset/weak-camels-roll.md b/.changeset/weak-camels-roll.md new file mode 100644 index 0000000000..90636605ec --- /dev/null +++ b/.changeset/weak-camels-roll.md @@ -0,0 +1,12 @@ +--- +'@backstage/create-app': patch +--- + +Updated the root `package.json` to use the new `backstage-cli repo clean` command. + +To apply this change to an existing project, make the following change to the root `package.json`: + +```diff +- "clean": "backstage-cli clean && lerna run clean", ++ "clean": "backstage-cli repo clean", +``` diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 47fb0d622f..26c6a61972 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -13,7 +13,7 @@ "build-image": "yarn workspace backend build-image", "tsc": "tsc", "tsc:full": "tsc --skipLibCheck false --incremental false", - "clean": "backstage-cli clean && lerna run clean", + "clean": "backstage-cli repo clean", "diff": "lerna run diff --", "test": "backstage-cli test", "test:all": "lerna run test -- --coverage", From fc3bdda6af76872111f3f2b32ecaa39bac75ee15 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 31 Aug 2022 22:03:59 +0200 Subject: [PATCH 10/25] root: update clean script to use backstage-cli repo clean Signed-off-by: Patrik Oldsberg --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1535f7a5e6..a2e028ffde 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts", "build:api-docs": "yarn build:api-reports --docs", "tsc": "tsc", - "tsc:full": "backstage-cli clean && tsc --skipLibCheck false --incremental false", - "clean": "backstage-cli clean && lerna run clean", + "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false", + "clean": "backstage-cli repo clean", "diff": "lerna run diff --", "test": "backstage-cli test", "test:all": "lerna run test -- --coverage", From cffea140dd57d1339be7c451fcbdf174fa06f65a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:20:55 +0000 Subject: [PATCH 11/25] fix(deps): update dependency jose to v4.9.2 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2445084328..74f6effd0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27521,9 +27521,9 @@ __metadata: linkType: hard "jose@npm:^4.6.0": - version: 4.9.1 - resolution: "jose@npm:4.9.1" - checksum: ebd9a4c9610d7fb93e9385f042554f0f3a83b16f3932a0aa42a7b78499c2ffae0607bce6b50a256521b3bf700d65140fdfb8de73988f1f29100491f3312a3707 + version: 4.9.2 + resolution: "jose@npm:4.9.2" + checksum: d3950385a6417d988c50bd8ba5407f5960624060aa8e4662c2109f1ebcc40c418e64b721a87065d8197b4aa0ddd7fb4dad5064618956dba8e74dc630916bf40f languageName: node linkType: hard From 412825d9ebfcf06eb1026992d178924f6cc954ef Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Aug 2022 12:42:38 +0200 Subject: [PATCH 12/25] feat(search-react): add use parent context prop Signed-off-by: Camila Belo --- .../src/context/SearchContext.tsx | 66 ++++++++++++------- .../SearchModal/SearchModal.test.tsx | 4 +- .../components/SearchModal/SearchModal.tsx | 16 +---- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index faad50f37a..41af7635c9 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -101,28 +101,20 @@ const searchInitialState: SearchContextState = { }; /** - * Props for {@link SearchContextProvider} - * - * @public + * Creates a new local search context. + * @remarks Use it for isolating this context from parent search contexts. + * @internal */ -export type SearchContextProviderProps = PropsWithChildren<{ - initialState?: SearchContextState; -}>; - -/** - * @public - * - * Search context provider which gives you access to shared state between search components - */ -export const SearchContextProvider = (props: SearchContextProviderProps) => { - const { initialState = searchInitialState, children } = props; +const useSearchContextValue = ( + initialValue: SearchContextState = searchInitialState, +) => { const searchApi = useApi(searchApiRef); const [pageCursor, setPageCursor] = useState( - initialState.pageCursor, + initialValue.pageCursor, ); - const [filters, setFilters] = useState(initialState.filters); - const [term, setTerm] = useState(initialState.term); - const [types, setTypes] = useState(initialState.types); + const [filters, setFilters] = useState(initialValue.filters); + const [term, setTerm] = useState(initialValue.term); + const [types, setTypes] = useState(initialValue.types); const prevTerm = usePrevious(term); @@ -170,11 +162,41 @@ export const SearchContextProvider = (props: SearchContextProviderProps) => { fetchPreviousPage: hasPreviousPage ? fetchPreviousPage : undefined, }; - const versionedValue = createVersionedValueMap({ 1: value }); + return value; +}; - return ( - - +/** + * Props for {@link SearchContextProvider} + * + * @public + */ +export type SearchContextProviderProps = PropsWithChildren<{ + initialState?: SearchContextState; + /** + * If true, don't create a child context if there is a parent one already defined. + * @remarks Default to false. + */ + useParentContext?: boolean; +}>; + +/** + * @public + * Search context provider which gives you access to shared state between search components + */ +export const SearchContextProvider = (props: SearchContextProviderProps) => { + const { initialState, useParentContext, children } = props; + const hasParentContext = useSearchContextCheck(); + const value = useSearchContextValue(initialState); + + return useParentContext && hasParentContext ? ( + <>{children} + ) : ( + + + {children} + ); }; diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 3ef9291585..2a6be75efb 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -55,7 +55,7 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(query).toHaveBeenCalledTimes(1); + expect(query).toHaveBeenCalledTimes(2); }); it('Should use parent search context if defined', async () => { @@ -133,7 +133,7 @@ describe('SearchModal', () => { }, ); - expect(query).toHaveBeenCalledTimes(1); + expect(query).toHaveBeenCalledTimes(2); await userEvent.keyboard('{Escape}'); expect(toggleModal).toHaveBeenCalledTimes(1); }); diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index dbb8100ec1..b7f2162bac 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; +import React from 'react'; import { Dialog, DialogActions, @@ -35,7 +35,6 @@ import { SearchResult, SearchResultPager, useSearch, - useSearchContextCheck, } from '@backstage/plugin-search-react'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Link, useContent } from '@backstage/core-components'; @@ -171,15 +170,6 @@ export const Modal = ({ toggleModal }: SearchModalProps) => { ); }; -const Context = ({ children }: PropsWithChildren<{}>) => { - // Checks if there is a parent context already defined and, if not, creates a new local context. - const hasParentContext = useSearchContextCheck(); - if (hasParentContext) { - return <>{children}; - } - return {children}; -}; - /** * @public */ @@ -204,11 +194,11 @@ export const SearchModal = ({ hidden={hidden} > {open && ( - + {(children && children({ toggleModal })) ?? ( )} - + )} ); From c32945d7874ad637fb39ab5477ffc3baa0ddc843 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Aug 2022 13:27:46 +0200 Subject: [PATCH 13/25] feat(search-react): create search bar autocomplete Signed-off-by: Camila Belo --- .../SearchAutocomplete.test.tsx | 233 ++++++++++++++++++ .../SearchAutocomplete/SearchAutocomplete.tsx | 202 +++++++++++++++ .../components/SearchAutocomplete/index.ts | 25 ++ .../src/components/SearchBar/SearchBar.tsx | 233 ++++++++++-------- .../src/components/SearchBar/index.tsx | 1 + plugins/search-react/src/components/index.ts | 3 +- 6 files changed, 592 insertions(+), 105 deletions(-) create mode 100644 plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx create mode 100644 plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.tsx create mode 100644 plugins/search-react/src/components/SearchAutocomplete/index.ts diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx new file mode 100644 index 0000000000..4502a61d6e --- /dev/null +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx @@ -0,0 +1,233 @@ +/* + * 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, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import LabelIcon from '@material-ui/icons/Label'; + +import { configApiRef } from '@backstage/core-plugin-api'; +import { ConfigReader } from '@backstage/core-app-api'; +import { TestApiProvider, renderWithEffects } from '@backstage/test-utils'; + +import { searchApiRef } from '../../api'; +import { + SearchAutocomplete, + SearchAutocompleteDefaultOption, +} from './SearchAutocomplete'; + +const title = 'Backstage Test App'; +const configApiMock = new ConfigReader({ + app: { title }, +}); + +const query = jest.fn().mockResolvedValue({ results: [] }); +const searchApiMock = { query }; + +describe('SearchAutocomplete', () => { + const options = ['hello-world', 'petstore', 'spotify']; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Renders without exploding', async () => { + await renderWithEffects( + + + , + ); + + expect(screen.getByTestId('search-autocomplete')).toBeInTheDocument(); + }); + + it('Show all options by default when focused', async () => { + await renderWithEffects( + + + , + ); + + expect(screen.queryByText(options[0])).not.toBeInTheDocument(); + expect(screen.queryByText(options[1])).not.toBeInTheDocument(); + expect(screen.queryByText(options[2])).not.toBeInTheDocument(); + + await userEvent.click(screen.getByPlaceholderText(`Search in ${title}`)); + + await waitFor(() => { + expect(screen.getByText(options[0])).toBeInTheDocument(); + expect(screen.getByText(options[1])).toBeInTheDocument(); + expect(screen.getByText(options[2])).toBeInTheDocument(); + }); + }); + + it('Updates context with the initial value', async () => { + await renderWithEffects( + + + , + ); + + await waitFor(() => { + expect(query).toBeCalledWith({ + filters: {}, + pageCursor: undefined, + term: options[0], + types: [], + }); + }); + }); + + it('Updates context when value is cleared', async () => { + await renderWithEffects( + + + , + ); + + await waitFor(() => { + expect(query).toBeCalledWith({ + filters: {}, + pageCursor: undefined, + term: options[0], + types: [], + }); + }); + + await userEvent.click(screen.getByLabelText('Clear')); + + await waitFor(() => { + expect(query).toBeCalledWith({ + filters: {}, + pageCursor: undefined, + term: '', + types: [], + }); + }); + }); + + it('Updates context when an option is select', async () => { + await renderWithEffects( + + + , + ); + + await userEvent.click(screen.getByPlaceholderText(`Search in ${title}`)); + + await userEvent.click(screen.getByText(options[0])); + + await waitFor(() => { + expect(query).toBeCalledWith({ + filters: {}, + pageCursor: undefined, + term: options[0], + types: [], + }); + }); + }); + + it('Shows a circular progress when loading options', async () => { + await renderWithEffects( + + + , + ); + + await waitFor(() => { + expect( + screen.getByTestId('search-autocomplete-progressbar'), + ).toBeInTheDocument(); + }); + }); + + it('Uses the default search autocomplete option component', async () => { + await renderWithEffects( + + option.title} + renderOption={option => ( + } + primaryText={option.title} + secondaryText={option.text} + /> + )} + /> + , + ); + + await userEvent.click(screen.getByPlaceholderText(`Search in ${title}`)); + + await waitFor(() => { + expect(screen.getAllByTitle('Option icon')).toHaveLength(3); + expect(screen.getByText('hello-world')).toBeInTheDocument(); + expect( + screen.getByText('Hello World example for gRPC'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.tsx new file mode 100644 index 0000000000..1068fefa50 --- /dev/null +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.tsx @@ -0,0 +1,202 @@ +/* + * 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, ReactNode, useCallback, useMemo } from 'react'; + +import { + CircularProgress, + ListItemIcon, + ListItemText, + ListItemTextProps, +} from '@material-ui/core'; +import { + Value, + Autocomplete, + AutocompleteProps, + AutocompleteChangeDetails, + AutocompleteChangeReason, + AutocompleteRenderInputParams, +} from '@material-ui/lab'; + +import { SearchContextProvider, useSearch } from '../../context'; +import { SearchBar, SearchBarProps } from '../SearchBar'; + +/** + * Props for {@link SearchAutocomplete}. + * + * @public + */ +export type SearchAutocompleteProps< + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, +> = Omit< + AutocompleteProps, + 'renderInput' +> & { + inputDebounceTime?: SearchBarProps['debounceTime']; + renderInput?: (params: AutocompleteRenderInputParams) => JSX.Element; +}; + +const withContext = ( + Component: < + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, + >( + props: SearchAutocompleteProps, + ) => JSX.Element, +) => { + return < + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, + >( + props: SearchAutocompleteProps, + ) => ( + + + + ); +}; + +/** + * Recommended search autocomplete when you use the Search Provider or Search Context. + * + * @public + */ +export const SearchAutocomplete = withContext( + < + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, + >({ + loading, + value, + onChange = () => {}, + options = [], + getOptionLabel = (option: T) => String(option), + renderInput, + inputDebounceTime, + fullWidth = true, + clearOnBlur = false, + ...rest + }: SearchAutocompleteProps) => { + const { setTerm } = useSearch(); + + const inputValue = useMemo(() => { + return value ? getOptionLabel(value as T) : ''; + }, [value, getOptionLabel]); + + const handleChange = useCallback( + ( + event: ChangeEvent<{}>, + newValue: Value, + reason: AutocompleteChangeReason, + details?: AutocompleteChangeDetails, + ) => { + onChange(event, newValue, reason, details); + setTerm(newValue ? getOptionLabel(newValue as T) : ''); + }, + [getOptionLabel, setTerm, onChange], + ); + + const defaultRenderInput = useCallback( + ({ + InputProps: { ref, endAdornment }, + InputLabelProps, + ...params + }: AutocompleteRenderInputParams) => ( + + ) : ( + endAdornment + ) + } + /> + ), + [loading, inputValue, inputDebounceTime], + ); + + return ( + + ); + }, +); + +/** + * Props for {@link SearchAutocompleteDefaultOption}. + * + * @public + */ +export type SearchAutocompleteDefaultOptionProps = { + icon?: ReactNode; + primaryText: ListItemTextProps['primary']; + primaryTextTypographyProps?: ListItemTextProps['primaryTypographyProps']; + secondaryText?: ListItemTextProps['secondary']; + secondaryTextTypographyProps?: ListItemTextProps['secondaryTypographyProps']; + disableTextTypography?: ListItemTextProps['disableTypography']; +}; + +/** + * A default search bar autocomplete component. + * + * @public + */ +export const SearchAutocompleteDefaultOption = ({ + icon, + primaryText, + primaryTextTypographyProps, + secondaryText, + secondaryTextTypographyProps, + disableTextTypography, +}: SearchAutocompleteDefaultOptionProps) => ( + <> + {icon ? {icon} : null} + + +); diff --git a/plugins/search-react/src/components/SearchAutocomplete/index.ts b/plugins/search-react/src/components/SearchAutocomplete/index.ts new file mode 100644 index 0000000000..c5534cef33 --- /dev/null +++ b/plugins/search-react/src/components/SearchAutocomplete/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { + SearchAutocomplete, + SearchAutocompleteDefaultOption, +} from './SearchAutocomplete'; + +export type { + SearchAutocompleteProps, + SearchAutocompleteDefaultOptionProps, +} from './SearchAutocomplete'; diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index d3d25455ce..9e91f3f965 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -20,8 +20,11 @@ import React, { useState, useEffect, useCallback, + forwardRef, + ComponentType, } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; + import { InputBase, InputBaseProps, @@ -37,11 +40,7 @@ import { useApi, } from '@backstage/core-plugin-api'; -import { - SearchContextProvider, - useSearch, - useSearchContextCheck, -} from '../../context'; +import { SearchContextProvider, useSearch } from '../../context'; import { TrackSearch } from '../SearchTracker'; /** @@ -64,94 +63,97 @@ export type SearchBarBaseProps = Omit & { * * @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(); +export const SearchBarBase = forwardRef( + ( + { + onChange, + onKeyDown = () => {}, + onClear = () => {}, + onSubmit = () => {}, + debounceTime = 200, + clearButton = true, + fullWidth = true, + value: defaultValue, + inputProps: defaultInputProps = {}, + endAdornment: defaultEndAdornment, + ...props + }, + ref, + ) => { + const configApi = useApi(configApiRef); + const [value, setValue] = useState(''); - useEffect(() => { - setValue(prevValue => - prevValue !== defaultValue ? (defaultValue as string) : prevValue, + useEffect(() => { + setValue(prevValue => + prevValue !== defaultValue ? String(defaultValue) : prevValue, + ); + }, [defaultValue]); + + useDebounce(() => onChange(value), debounceTime, [value]); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setValue(e.target.value); + }, + [setValue], ); - }, [defaultValue]); - useDebounce(() => onChange(value), debounceTime, [value]); + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (onKeyDown) onKeyDown(e); + if (onSubmit && e.key === 'Enter') { + onSubmit(); + } + }, + [onKeyDown, onSubmit], + ); - 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(); + const handleClear = useCallback(() => { + onChange(''); + if (onClear) { + onClear(); } - }, - [onKeyDown, onSubmit], - ); + }, [onChange, onClear]); - const handleClear = useCallback(() => { - onChange(''); - }, [onChange]); + const placeholder = `Search in ${ + configApi.getOptionalString('app.title') || 'Backstage' + }`; - const placeholder = `Search in ${ - configApi.getOptionalString('app.title') || 'Backstage' - }`; + const startAdornment = ( + + + + + + ); - const startAdornment = ( - - - - - - ); + const endAdornment = ( + + + + + + ); - const endAdornment = ( - - - - - - ); - - const searchBar = ( - - - - ); - - return hasSearchContext ? ( - searchBar - ) : ( - {searchBar} - ); -}; + return ( + + + + ); + }, +); /** * Props for {@link SearchBar}. @@ -160,30 +162,53 @@ export const SearchBarBase = ({ */ export type SearchBarProps = Partial; +const withContext = (Component: ComponentType) => { + return forwardRef((props, ref) => ( + + + + )); +}; + /** * Recommended search bar when you use the Search Provider or Search Context. * * @public */ -export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { - const { term, setTerm } = useSearch(); +export const SearchBar = withContext( + forwardRef( + ({ value: initialValue = '', onChange, ...rest }, ref) => { + const { term, setTerm } = useSearch(); - const handleChange = useCallback( - (newValue: string) => { - if (onChange) { - onChange(newValue); - } else { - setTerm(newValue); - } + useEffect(() => { + if (initialValue) { + setTerm(String(initialValue)); + } + }, [initialValue, setTerm]); + + const handleChange = useCallback( + (newValue: string) => { + if (onChange) { + onChange(newValue); + } else { + setTerm(newValue); + } + }, + [onChange, setTerm], + ); + + return ( + + + + ); }, - [onChange, setTerm], - ); - - return ( - - - - ); -}; + ), +); diff --git a/plugins/search-react/src/components/SearchBar/index.tsx b/plugins/search-react/src/components/SearchBar/index.tsx index 075a0c7dc2..928543916a 100644 --- a/plugins/search-react/src/components/SearchBar/index.tsx +++ b/plugins/search-react/src/components/SearchBar/index.tsx @@ -15,4 +15,5 @@ */ export { SearchBar, SearchBarBase } from './SearchBar'; + export type { SearchBarProps, SearchBarBaseProps } from './SearchBar'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 263ca4ad59..e254c36dd9 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -15,8 +15,9 @@ */ export * from './HighlightedSearchResultText'; +export * from './SearchBar'; +export * from './SearchAutocomplete'; export * from './SearchFilter'; export * from './SearchResult'; export * from './SearchResultPager'; -export * from './SearchBar'; export * from './DefaultResultListItem'; From 43e375eecab4a99fd40566a4509b4e72250a1d68 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 29 Aug 2022 10:45:36 +0200 Subject: [PATCH 14/25] chore: update api reports Signed-off-by: Camila Belo --- plugins/search-react/api-report.md | 635 ++++++++++++++++++++++++++++- 1 file changed, 622 insertions(+), 13 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index e4afee45ca..fcb0378ab0 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -7,8 +7,11 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; +import { AutocompleteProps } from '@material-ui/lab'; +import { AutocompleteRenderInputParams } from '@material-ui/lab'; import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; +import { ListItemTextProps } from '@material-ui/core'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; @@ -74,6 +77,36 @@ export interface SearchApi { // @public (undocumented) export const searchApiRef: ApiRef; +// @public +export const SearchAutocomplete: < + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, +>( + props: SearchAutocompleteProps, +) => JSX.Element; + +// @public +export const SearchAutocompleteDefaultOption: ({ + icon, + primaryText, + primaryTextTypographyProps, + secondaryText, + secondaryTextTypographyProps, + disableTextTypography, +}: SearchAutocompleteDefaultOptionProps) => JSX.Element; + +// @public +export type SearchAutocompleteDefaultOptionProps = { + icon?: ReactNode; + primaryText: ListItemTextProps['primary']; + primaryTextTypographyProps?: ListItemTextProps['primaryTypographyProps']; + secondaryText?: ListItemTextProps['secondary']; + secondaryTextTypographyProps?: ListItemTextProps['secondaryTypographyProps']; + disableTextTypography?: ListItemTextProps['disableTypography']; +}; + // @public (undocumented) export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { filterSelectedOptions?: boolean; @@ -82,21 +115,596 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { }; // @public -export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; +export type SearchAutocompleteProps< + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, +> = Omit< + AutocompleteProps, + 'renderInput' +> & { + inputDebounceTime?: SearchBarProps['debounceTime']; + renderInput?: (params: AutocompleteRenderInputParams) => JSX.Element; +}; // @public -export const SearchBarBase: ({ - onChange, - onKeyDown, - onSubmit, - debounceTime, - clearButton, - fullWidth, - value: defaultValue, - inputProps: defaultInputProps, - endAdornment: defaultEndAdornment, - ...props -}: SearchBarBaseProps) => JSX.Element; +export const SearchBar: React_2.ForwardRefExoticComponent< + Pick< + Partial, + | 'required' + | 'type' + | 'error' + | 'id' + | 'name' + | 'color' + | 'margin' + | 'translate' + | 'value' + | 'hidden' + | 'dir' + | 'slot' + | 'style' + | 'title' + | 'accessKey' + | 'draggable' + | 'lang' + | 'className' + | 'prefix' + | 'contentEditable' + | 'inputMode' + | 'tabIndex' + | 'disabled' + | 'autoComplete' + | 'autoFocus' + | 'defaultChecked' + | 'defaultValue' + | 'suppressContentEditableWarning' + | 'suppressHydrationWarning' + | 'contextMenu' + | 'placeholder' + | 'spellCheck' + | 'radioGroup' + | 'role' + | 'about' + | 'datatype' + | 'inlist' + | 'property' + | 'resource' + | 'typeof' + | 'vocab' + | 'autoCapitalize' + | 'autoCorrect' + | 'autoSave' + | 'itemProp' + | 'itemScope' + | 'itemType' + | 'itemID' + | 'itemRef' + | 'results' + | 'security' + | 'unselectable' + | 'is' + | 'aria-activedescendant' + | 'aria-atomic' + | 'aria-autocomplete' + | 'aria-busy' + | 'aria-checked' + | 'aria-colcount' + | 'aria-colindex' + | 'aria-colspan' + | 'aria-controls' + | 'aria-current' + | 'aria-describedby' + | 'aria-details' + | 'aria-disabled' + | 'aria-dropeffect' + | 'aria-errormessage' + | 'aria-expanded' + | 'aria-flowto' + | 'aria-grabbed' + | 'aria-haspopup' + | 'aria-hidden' + | 'aria-invalid' + | 'aria-keyshortcuts' + | 'aria-label' + | 'aria-labelledby' + | 'aria-level' + | 'aria-live' + | 'aria-modal' + | 'aria-multiline' + | 'aria-multiselectable' + | 'aria-orientation' + | 'aria-owns' + | 'aria-placeholder' + | 'aria-posinset' + | 'aria-pressed' + | 'aria-readonly' + | 'aria-relevant' + | 'aria-required' + | 'aria-roledescription' + | 'aria-rowcount' + | 'aria-rowindex' + | 'aria-rowspan' + | 'aria-selected' + | 'aria-setsize' + | 'aria-sort' + | 'aria-valuemax' + | 'aria-valuemin' + | 'aria-valuenow' + | 'aria-valuetext' + | 'readOnly' + | 'rows' + | 'dangerouslySetInnerHTML' + | 'onCopy' + | 'onCopyCapture' + | 'onCut' + | 'onCutCapture' + | 'onPaste' + | 'onPasteCapture' + | 'onCompositionEnd' + | 'onCompositionEndCapture' + | 'onCompositionStart' + | 'onCompositionStartCapture' + | 'onCompositionUpdate' + | 'onCompositionUpdateCapture' + | 'onFocus' + | 'onFocusCapture' + | 'onBlur' + | 'onBlurCapture' + | 'onChange' + | 'onChangeCapture' + | 'onBeforeInput' + | 'onBeforeInputCapture' + | 'onInput' + | 'onInputCapture' + | 'onReset' + | 'onResetCapture' + | 'onSubmit' + | 'onSubmitCapture' + | 'onInvalid' + | 'onInvalidCapture' + | 'onLoad' + | 'onLoadCapture' + | 'onError' + | 'onErrorCapture' + | 'onKeyDown' + | 'onKeyDownCapture' + | 'onKeyPress' + | 'onKeyPressCapture' + | 'onKeyUp' + | 'onKeyUpCapture' + | 'onAbort' + | 'onAbortCapture' + | 'onCanPlay' + | 'onCanPlayCapture' + | 'onCanPlayThrough' + | 'onCanPlayThroughCapture' + | 'onDurationChange' + | 'onDurationChangeCapture' + | 'onEmptied' + | 'onEmptiedCapture' + | 'onEncrypted' + | 'onEncryptedCapture' + | 'onEnded' + | 'onEndedCapture' + | 'onLoadedData' + | 'onLoadedDataCapture' + | 'onLoadedMetadata' + | 'onLoadedMetadataCapture' + | 'onLoadStart' + | 'onLoadStartCapture' + | 'onPause' + | 'onPauseCapture' + | 'onPlay' + | 'onPlayCapture' + | 'onPlaying' + | 'onPlayingCapture' + | 'onProgress' + | 'onProgressCapture' + | 'onRateChange' + | 'onRateChangeCapture' + | 'onSeeked' + | 'onSeekedCapture' + | 'onSeeking' + | 'onSeekingCapture' + | 'onStalled' + | 'onStalledCapture' + | 'onSuspend' + | 'onSuspendCapture' + | 'onTimeUpdate' + | 'onTimeUpdateCapture' + | 'onVolumeChange' + | 'onVolumeChangeCapture' + | 'onWaiting' + | 'onWaitingCapture' + | 'onAuxClick' + | 'onAuxClickCapture' + | 'onClick' + | 'onClickCapture' + | 'onContextMenu' + | 'onContextMenuCapture' + | 'onDoubleClick' + | 'onDoubleClickCapture' + | 'onDrag' + | 'onDragCapture' + | 'onDragEnd' + | 'onDragEndCapture' + | 'onDragEnter' + | 'onDragEnterCapture' + | 'onDragExit' + | 'onDragExitCapture' + | 'onDragLeave' + | 'onDragLeaveCapture' + | 'onDragOver' + | 'onDragOverCapture' + | 'onDragStart' + | 'onDragStartCapture' + | 'onDrop' + | 'onDropCapture' + | 'onMouseDown' + | 'onMouseDownCapture' + | 'onMouseEnter' + | 'onMouseLeave' + | 'onMouseMove' + | 'onMouseMoveCapture' + | 'onMouseOut' + | 'onMouseOutCapture' + | 'onMouseOver' + | 'onMouseOverCapture' + | 'onMouseUp' + | 'onMouseUpCapture' + | 'onSelect' + | 'onSelectCapture' + | 'onTouchCancel' + | 'onTouchCancelCapture' + | 'onTouchEnd' + | 'onTouchEndCapture' + | 'onTouchMove' + | 'onTouchMoveCapture' + | 'onTouchStart' + | 'onTouchStartCapture' + | 'onPointerDown' + | 'onPointerDownCapture' + | 'onPointerMove' + | 'onPointerMoveCapture' + | 'onPointerUp' + | 'onPointerUpCapture' + | 'onPointerCancel' + | 'onPointerCancelCapture' + | 'onPointerEnter' + | 'onPointerEnterCapture' + | 'onPointerLeave' + | 'onPointerLeaveCapture' + | 'onPointerOver' + | 'onPointerOverCapture' + | 'onPointerOut' + | 'onPointerOutCapture' + | 'onGotPointerCapture' + | 'onGotPointerCaptureCapture' + | 'onLostPointerCapture' + | 'onLostPointerCaptureCapture' + | 'onScroll' + | 'onScrollCapture' + | 'onWheel' + | 'onWheelCapture' + | 'onAnimationStart' + | 'onAnimationStartCapture' + | 'onAnimationEnd' + | 'onAnimationEndCapture' + | 'onAnimationIteration' + | 'onAnimationIterationCapture' + | 'onTransitionEnd' + | 'onTransitionEndCapture' + | 'classes' + | 'innerRef' + | 'fullWidth' + | 'inputProps' + | 'inputRef' + | 'multiline' + | 'endAdornment' + | 'inputComponent' + | 'renderSuffix' + | 'rowsMax' + | 'rowsMin' + | 'maxRows' + | 'minRows' + | 'startAdornment' + | 'onClear' + | 'debounceTime' + | 'clearButton' + > & + React_2.RefAttributes +>; + +// @public +export const SearchBarBase: React_2.ForwardRefExoticComponent< + Pick< + SearchBarBaseProps, + | 'required' + | 'type' + | 'error' + | 'id' + | 'name' + | 'color' + | 'margin' + | 'translate' + | 'value' + | 'hidden' + | 'dir' + | 'slot' + | 'style' + | 'title' + | 'accessKey' + | 'draggable' + | 'lang' + | 'className' + | 'prefix' + | 'contentEditable' + | 'inputMode' + | 'tabIndex' + | 'disabled' + | 'autoComplete' + | 'autoFocus' + | 'defaultChecked' + | 'defaultValue' + | 'suppressContentEditableWarning' + | 'suppressHydrationWarning' + | 'contextMenu' + | 'placeholder' + | 'spellCheck' + | 'radioGroup' + | 'role' + | 'about' + | 'datatype' + | 'inlist' + | 'property' + | 'resource' + | 'typeof' + | 'vocab' + | 'autoCapitalize' + | 'autoCorrect' + | 'autoSave' + | 'itemProp' + | 'itemScope' + | 'itemType' + | 'itemID' + | 'itemRef' + | 'results' + | 'security' + | 'unselectable' + | 'is' + | 'aria-activedescendant' + | 'aria-atomic' + | 'aria-autocomplete' + | 'aria-busy' + | 'aria-checked' + | 'aria-colcount' + | 'aria-colindex' + | 'aria-colspan' + | 'aria-controls' + | 'aria-current' + | 'aria-describedby' + | 'aria-details' + | 'aria-disabled' + | 'aria-dropeffect' + | 'aria-errormessage' + | 'aria-expanded' + | 'aria-flowto' + | 'aria-grabbed' + | 'aria-haspopup' + | 'aria-hidden' + | 'aria-invalid' + | 'aria-keyshortcuts' + | 'aria-label' + | 'aria-labelledby' + | 'aria-level' + | 'aria-live' + | 'aria-modal' + | 'aria-multiline' + | 'aria-multiselectable' + | 'aria-orientation' + | 'aria-owns' + | 'aria-placeholder' + | 'aria-posinset' + | 'aria-pressed' + | 'aria-readonly' + | 'aria-relevant' + | 'aria-required' + | 'aria-roledescription' + | 'aria-rowcount' + | 'aria-rowindex' + | 'aria-rowspan' + | 'aria-selected' + | 'aria-setsize' + | 'aria-sort' + | 'aria-valuemax' + | 'aria-valuemin' + | 'aria-valuenow' + | 'aria-valuetext' + | 'readOnly' + | 'rows' + | 'dangerouslySetInnerHTML' + | 'onCopy' + | 'onCopyCapture' + | 'onCut' + | 'onCutCapture' + | 'onPaste' + | 'onPasteCapture' + | 'onCompositionEnd' + | 'onCompositionEndCapture' + | 'onCompositionStart' + | 'onCompositionStartCapture' + | 'onCompositionUpdate' + | 'onCompositionUpdateCapture' + | 'onFocus' + | 'onFocusCapture' + | 'onBlur' + | 'onBlurCapture' + | 'onChange' + | 'onChangeCapture' + | 'onBeforeInput' + | 'onBeforeInputCapture' + | 'onInput' + | 'onInputCapture' + | 'onReset' + | 'onResetCapture' + | 'onSubmit' + | 'onSubmitCapture' + | 'onInvalid' + | 'onInvalidCapture' + | 'onLoad' + | 'onLoadCapture' + | 'onError' + | 'onErrorCapture' + | 'onKeyDown' + | 'onKeyDownCapture' + | 'onKeyPress' + | 'onKeyPressCapture' + | 'onKeyUp' + | 'onKeyUpCapture' + | 'onAbort' + | 'onAbortCapture' + | 'onCanPlay' + | 'onCanPlayCapture' + | 'onCanPlayThrough' + | 'onCanPlayThroughCapture' + | 'onDurationChange' + | 'onDurationChangeCapture' + | 'onEmptied' + | 'onEmptiedCapture' + | 'onEncrypted' + | 'onEncryptedCapture' + | 'onEnded' + | 'onEndedCapture' + | 'onLoadedData' + | 'onLoadedDataCapture' + | 'onLoadedMetadata' + | 'onLoadedMetadataCapture' + | 'onLoadStart' + | 'onLoadStartCapture' + | 'onPause' + | 'onPauseCapture' + | 'onPlay' + | 'onPlayCapture' + | 'onPlaying' + | 'onPlayingCapture' + | 'onProgress' + | 'onProgressCapture' + | 'onRateChange' + | 'onRateChangeCapture' + | 'onSeeked' + | 'onSeekedCapture' + | 'onSeeking' + | 'onSeekingCapture' + | 'onStalled' + | 'onStalledCapture' + | 'onSuspend' + | 'onSuspendCapture' + | 'onTimeUpdate' + | 'onTimeUpdateCapture' + | 'onVolumeChange' + | 'onVolumeChangeCapture' + | 'onWaiting' + | 'onWaitingCapture' + | 'onAuxClick' + | 'onAuxClickCapture' + | 'onClick' + | 'onClickCapture' + | 'onContextMenu' + | 'onContextMenuCapture' + | 'onDoubleClick' + | 'onDoubleClickCapture' + | 'onDrag' + | 'onDragCapture' + | 'onDragEnd' + | 'onDragEndCapture' + | 'onDragEnter' + | 'onDragEnterCapture' + | 'onDragExit' + | 'onDragExitCapture' + | 'onDragLeave' + | 'onDragLeaveCapture' + | 'onDragOver' + | 'onDragOverCapture' + | 'onDragStart' + | 'onDragStartCapture' + | 'onDrop' + | 'onDropCapture' + | 'onMouseDown' + | 'onMouseDownCapture' + | 'onMouseEnter' + | 'onMouseLeave' + | 'onMouseMove' + | 'onMouseMoveCapture' + | 'onMouseOut' + | 'onMouseOutCapture' + | 'onMouseOver' + | 'onMouseOverCapture' + | 'onMouseUp' + | 'onMouseUpCapture' + | 'onSelect' + | 'onSelectCapture' + | 'onTouchCancel' + | 'onTouchCancelCapture' + | 'onTouchEnd' + | 'onTouchEndCapture' + | 'onTouchMove' + | 'onTouchMoveCapture' + | 'onTouchStart' + | 'onTouchStartCapture' + | 'onPointerDown' + | 'onPointerDownCapture' + | 'onPointerMove' + | 'onPointerMoveCapture' + | 'onPointerUp' + | 'onPointerUpCapture' + | 'onPointerCancel' + | 'onPointerCancelCapture' + | 'onPointerEnter' + | 'onPointerEnterCapture' + | 'onPointerLeave' + | 'onPointerLeaveCapture' + | 'onPointerOver' + | 'onPointerOverCapture' + | 'onPointerOut' + | 'onPointerOutCapture' + | 'onGotPointerCapture' + | 'onGotPointerCaptureCapture' + | 'onLostPointerCapture' + | 'onLostPointerCaptureCapture' + | 'onScroll' + | 'onScrollCapture' + | 'onWheel' + | 'onWheelCapture' + | 'onAnimationStart' + | 'onAnimationStartCapture' + | 'onAnimationEnd' + | 'onAnimationEndCapture' + | 'onAnimationIteration' + | 'onAnimationIterationCapture' + | 'onTransitionEnd' + | 'onTransitionEndCapture' + | 'classes' + | 'innerRef' + | 'fullWidth' + | 'inputProps' + | 'inputRef' + | 'multiline' + | 'endAdornment' + | 'inputComponent' + | 'renderSuffix' + | 'rowsMax' + | 'rowsMin' + | 'maxRows' + | 'minRows' + | 'startAdornment' + | 'onClear' + | 'debounceTime' + | 'clearButton' + > & + React_2.RefAttributes +>; // @public export type SearchBarBaseProps = Omit & { @@ -118,6 +726,7 @@ export const SearchContextProvider: ( // @public export type SearchContextProviderProps = PropsWithChildren<{ initialState?: SearchContextState; + useParentContext?: boolean; }>; // @public (undocumented) From 18f60427f22c80e53133c8927edf606f2aa410bc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 29 Aug 2022 11:03:53 +0200 Subject: [PATCH 15/25] chore: add changeset files Signed-off-by: Camila Belo --- .changeset/search-feet-flash.md | 21 +++++++++++++++++++++ .changeset/search-planets-flash.md | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 .changeset/search-feet-flash.md create mode 100644 .changeset/search-planets-flash.md diff --git a/.changeset/search-feet-flash.md b/.changeset/search-feet-flash.md new file mode 100644 index 0000000000..fb4a27975d --- /dev/null +++ b/.changeset/search-feet-flash.md @@ -0,0 +1,21 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Add the term autocomplete functionality to the search bar with a `SearchAutocomplete` component. Additionally, we provide a `SearchAutocompleteDefaultOption` to render options with an icon, a primary text and a secondary text. +Example: + +```jsx +// import { SearchAutocomplete, SearchAutocompleteDefaultOption} from '@backstage/plugin-search-react'; + option.title} + renderOption={option => ( + } + primaryText={option.title} + secondaryText={option.text} + /> + )} +/> +``` diff --git a/.changeset/search-planets-flash.md b/.changeset/search-planets-flash.md new file mode 100644 index 0000000000..cc81b8c209 --- /dev/null +++ b/.changeset/search-planets-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Add `userParentContext` prop to the `SearchContextProvider`, this added property does not create a local context and consumes the parent if it already exists. From ca8d5a6eae130d0dce7bfb53c49ea6d76ec062c8 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 29 Aug 2022 15:07:18 +0200 Subject: [PATCH 16/25] refactor: apply review suggestions Signed-off-by: Camila Belo --- .changeset/search-feet-flash.md | 66 +- .changeset/search-planets-flash.md | 2 +- .changeset/search-zebras-tap.md | 7 + .changeset/techdocs-feet-dress.md | 5 + plugins/search-react/api-report.md | 617 +----------------- .../SearchAutocomplete.stories.tsx | 122 ++++ .../SearchAutocomplete.test.tsx | 14 +- .../SearchAutocomplete/SearchAutocomplete.tsx | 167 ++--- ...earchAutocompleteDefaultOption.stories.tsx | 104 +++ .../SearchAutocompleteDefaultOption.tsx | 61 ++ .../components/SearchAutocomplete/index.ts | 11 +- .../src/components/SearchBar/SearchBar.tsx | 248 +++---- .../src/context/SearchContext.tsx | 75 ++- .../SearchModal/SearchModal.test.tsx | 4 +- .../components/SearchModal/SearchModal.tsx | 2 +- .../src/search/components/TechDocsSearch.tsx | 147 ++--- 16 files changed, 684 insertions(+), 968 deletions(-) create mode 100644 .changeset/search-zebras-tap.md create mode 100644 .changeset/techdocs-feet-dress.md create mode 100644 plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx create mode 100644 plugins/search-react/src/components/SearchAutocomplete/SearchAutocompleteDefaultOption.stories.tsx create mode 100644 plugins/search-react/src/components/SearchAutocomplete/SearchAutocompleteDefaultOption.tsx diff --git a/.changeset/search-feet-flash.md b/.changeset/search-feet-flash.md index fb4a27975d..c7423f0317 100644 --- a/.changeset/search-feet-flash.md +++ b/.changeset/search-feet-flash.md @@ -1,21 +1,59 @@ --- -'@backstage/plugin-search-react': patch +'@backstage/plugin-search-react': minor --- -Add the term autocomplete functionality to the search bar with a `SearchAutocomplete` component. Additionally, we provide a `SearchAutocompleteDefaultOption` to render options with an icon, a primary text and a secondary text. +Provides search autocomplete functionality through a `SearchAutocomplete` component. +A `SearchAutocompleteDefaultOption` can also be used to render options with icons, primary texts, and secondary texts. Example: ```jsx -// import { SearchAutocomplete, SearchAutocompleteDefaultOption} from '@backstage/plugin-search-react'; - option.title} - renderOption={option => ( - } - primaryText={option.title} - secondaryText={option.text} - /> - )} -/> +import React, { ChangeEvent, useState, useCallback } from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { Grid, Paper } from '@material-ui/core'; + +import { Page, Content } from '@backstage/core-components'; +import { SearchAutocomplete, SearchAutocompleteDefaultOption} from '@backstage/plugin-search-react'; + +const OptionsIcon = () => + +const SearchPage = () => { + const [inputValue, setInputValue] = useState(''); + + const options = useAsync(async () => { + // Gets and returns autocomplete options + }, [inputValue]) + + const useCallback((_event: ChangeEvent<{}>, newInputValue: string) => { + setInputValue(newInputValue); + }, [setInputValue]) + + return ( + + + + + + option.title} + renderOption={option => ( + } + primaryText={option.title} + secondaryText={option.text} + /> + )} + /> + + + + {'/* Filters and results are omitted */'} + + + ); +}; ``` diff --git a/.changeset/search-planets-flash.md b/.changeset/search-planets-flash.md index cc81b8c209..49431128f9 100644 --- a/.changeset/search-planets-flash.md +++ b/.changeset/search-planets-flash.md @@ -2,4 +2,4 @@ '@backstage/plugin-search': patch --- -Add `userParentContext` prop to the `SearchContextProvider`, this added property does not create a local context and consumes the parent if it already exists. +Use the new `inheritParentContextIfAvailable` search context property in `SearchModal` instead of manually checking if a parent context exists, this conditional statement was previously duplicated in more than one component like in `SearchBar` as well and is now only done in ` SearchContextProvider`. diff --git a/.changeset/search-zebras-tap.md b/.changeset/search-zebras-tap.md new file mode 100644 index 0000000000..4593478a51 --- /dev/null +++ b/.changeset/search-zebras-tap.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search-react': minor +--- + +We noticed a repeated check for the existence of a parent context before creating a child search context in more the one component such as Search Modal and Search Bar and to remove code duplication we extract the conditional to the context provider, now you can use it passing an `inheritParentContextIfAvailable` prop to the `SearchContextProvider`. + +Note: This added property does not create a local context if there is a parent context and in this case, you cannot use it together with `initialState`, it will result in a type error because the parent context is already initialized. diff --git a/.changeset/techdocs-feet-dress.md b/.changeset/techdocs-feet-dress.md new file mode 100644 index 0000000000..d2b2a625af --- /dev/null +++ b/.changeset/techdocs-feet-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Use the new `SearchAutocomplete` component in the `TechDocsSearch` component to maintain consistency across search experiences and avoid code duplication. diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index fcb0378ab0..7d5e64757b 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -8,7 +8,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { AutocompleteProps } from '@material-ui/lab'; -import { AutocompleteRenderInputParams } from '@material-ui/lab'; +import { ForwardRefExoticComponent } from 'react'; import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { ListItemTextProps } from '@material-ui/core'; @@ -78,13 +78,11 @@ export interface SearchApi { export const searchApiRef: ApiRef; // @public -export const SearchAutocomplete: < - T, - Multiple extends boolean | undefined, - DisableClearable extends boolean | undefined, - FreeSolo extends boolean | undefined, ->( - props: SearchAutocompleteProps, +export const SearchAutocomplete: SearchAutocompleteComponent; + +// @public +export type SearchAutocompleteComponent =