From d90f71068a9ea1c3b3077ff4359784d6fd178937 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Tue, 7 Jan 2025 17:17:13 -0500 Subject: [PATCH] add blueprint extension for result type filter in search plugin Signed-off-by: Mark Dunphy --- plugins/search-react/report-alpha.api.md | 37 +++++++ .../SearchFilterResultTypeBlueprint.test.tsx | 104 ++++++++++++++++++ .../SearchFilterResultTypeBlueprint.tsx | 56 ++++++++++ .../src/alpha/blueprints/index.ts | 5 + .../src/alpha/blueprints/types.ts | 6 + plugins/search/report-alpha.api.md | 72 ++++++++---- plugins/search/src/alpha.tsx | 14 ++- 7 files changed, 271 insertions(+), 23 deletions(-) create mode 100644 plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.test.tsx create mode 100644 plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.tsx diff --git a/plugins/search-react/report-alpha.api.md b/plugins/search-react/report-alpha.api.md index e49b0bbeee..d8d0dbac97 100644 --- a/plugins/search-react/report-alpha.api.md +++ b/plugins/search-react/report-alpha.api.md @@ -17,6 +17,43 @@ export type BaseSearchResultListItemProps = T & { result?: SearchDocument; } & Omit; +// @alpha (undocumented) +export const SearchFilterResultTypeBlueprint: ExtensionBlueprint<{ + kind: 'search-filter-result-type'; + name: undefined; + params: SearchFilterResultTypeBlueprintParams; + output: ConfigurableExtensionDataRef< + { + value: string; + name: string; + icon: JSX.Element; + }, + 'search.filters.result-types.type', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + resultType: ConfigurableExtensionDataRef< + { + value: string; + name: string; + icon: JSX.Element; + }, + 'search.filters.result-types.type', + {} + >; + }; +}>; + +// @alpha (undocumented) +export interface SearchFilterResultTypeBlueprintParams { + icon: JSX.Element; + name: string; + value: string; +} + // @alpha (undocumented) export type SearchResultItemExtensionComponent = < P extends BaseSearchResultListItemProps, diff --git a/plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.test.tsx b/plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.test.tsx new file mode 100644 index 0000000000..b2de0319c6 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2025 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 { + PageBlueprint, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { SearchFilterResultTypeBlueprint } from './SearchFilterResultTypeBlueprint'; +import { searchResultTypeDataRef } from './types'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; + +describe('SearchFilterResultTypeBlueprint', () => { + it('should return an extension', () => { + const extension = SearchFilterResultTypeBlueprint.make({ + name: 'test', + params: { + value: 'test', + name: 'Test', + icon:
Hello
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "T": undefined, + "attachTo": { + "id": "page:search", + "input": "resultTypes", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "search-filter-result-type", + "name": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should render result types', async () => { + const extension = SearchFilterResultTypeBlueprint.make({ + name: 'test', + params: { + value: 'test', + name: 'Test Result Type', + icon:
Hello
, + }, + }); + + const searchPage = PageBlueprint.makeWithOverrides({ + name: 'search', + inputs: { + resultTypes: createExtensionInput([searchResultTypeDataRef]), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/', + loader: async () => { + const resultTypes = inputs.resultTypes.map(t => + t.get(searchResultTypeDataRef), + ); + return ( +
+ {resultTypes.map((t, i) => ( +
{t.name}
+ ))} +
+ ); + }, + }); + }, + }); + + await expect( + renderInTestApp( + createExtensionTester(searchPage).add(extension).reactElement(), + ).findByText('Test Result Type'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.tsx b/plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.tsx new file mode 100644 index 0000000000..fa321daa73 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/SearchFilterResultTypeBlueprint.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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 { createExtensionBlueprint } from '@backstage/frontend-plugin-api'; +import { searchResultTypeDataRef } from './types'; + +/** @alpha */ +export interface SearchFilterResultTypeBlueprintParams { + /** + * The value of the result type. + */ + value: string; + /** + * The name of the result type. + */ + name: string; + /** + * The icon of the result type. + */ + icon: JSX.Element; +} + +/** + * @alpha + */ +export const SearchFilterResultTypeBlueprint = createExtensionBlueprint({ + kind: 'search-filter-result-type', + attachTo: { + id: 'page:search', + input: 'resultTypes', + }, + output: [searchResultTypeDataRef], + dataRefs: { + resultType: searchResultTypeDataRef, + }, + *factory(params: SearchFilterResultTypeBlueprintParams) { + yield searchResultTypeDataRef({ + value: params.value, + name: params.name, + icon: params.icon, + }); + }, +}); diff --git a/plugins/search-react/src/alpha/blueprints/index.ts b/plugins/search-react/src/alpha/blueprints/index.ts index b9bbf1012e..ca78a6d603 100644 --- a/plugins/search-react/src/alpha/blueprints/index.ts +++ b/plugins/search-react/src/alpha/blueprints/index.ts @@ -23,3 +23,8 @@ export { type SearchResultItemExtensionComponent, type SearchResultItemExtensionPredicate, } from './types'; + +export { + SearchFilterResultTypeBlueprint, + type SearchFilterResultTypeBlueprintParams, +} from './SearchFilterResultTypeBlueprint'; diff --git a/plugins/search-react/src/alpha/blueprints/types.ts b/plugins/search-react/src/alpha/blueprints/types.ts index abfed4aba6..80888203a0 100644 --- a/plugins/search-react/src/alpha/blueprints/types.ts +++ b/plugins/search-react/src/alpha/blueprints/types.ts @@ -40,3 +40,9 @@ export const searchResultListItemDataRef = createExtensionDataRef<{ predicate?: SearchResultItemExtensionPredicate; component: SearchResultItemExtensionComponent; }>().with({ id: 'search.search-result-list-item.item' }); + +export const searchResultTypeDataRef = createExtensionDataRef<{ + value: string; + name: string; + icon: JSX.Element; +}>().with({ id: 'search.filters.result-types.type' }); diff --git a/plugins/search/report-alpha.api.md b/plugins/search/report-alpha.api.md index d4a016bbe2..885e6453f0 100644 --- a/plugins/search/report-alpha.api.md +++ b/plugins/search/report-alpha.api.md @@ -22,27 +22,6 @@ const _default: FrontendPlugin< }, {}, { - 'nav-item:search': ExtensionDefinition<{ - kind: 'nav-item'; - name: undefined; - config: {}; - configInput: {}; - output: ConfigurableExtensionDataRef< - { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }, - 'core.nav-item.target', - {} - >; - inputs: {}; - params: { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }; - }>; 'api:search': ExtensionDefinition<{ kind: 'api'; name: undefined; @@ -98,6 +77,21 @@ const _default: FrontendPlugin< optional: false; } >; + resultTypes: ExtensionInput< + ConfigurableExtensionDataRef< + { + value: string; + name: string; + icon: JSX.Element; + }, + 'search.filters.result-types.type', + {} + >, + { + singleton: false; + optional: false; + } + >; }; kind: 'page'; name: undefined; @@ -107,6 +101,27 @@ const _default: FrontendPlugin< routeRef?: RouteRef | undefined; }; }>; + 'nav-item:search': ExtensionDefinition<{ + kind: 'nav-item'; + name: undefined; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + inputs: {}; + params: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }; + }>; } >; export default _default; @@ -184,6 +199,21 @@ export const searchPage: ExtensionDefinition<{ optional: false; } >; + resultTypes: ExtensionInput< + ConfigurableExtensionDataRef< + { + value: string; + name: string; + icon: JSX.Element; + }, + 'search.filters.result-types.type', + {} + >, + { + singleton: false; + optional: false; + } + >; }; kind: 'page'; name: undefined; diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 90fc394f9d..8bbcfb1048 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -61,7 +61,10 @@ import { } from '@backstage/plugin-search-react'; import { SearchResult } from '@backstage/plugin-search-common'; import { searchApiRef } from '@backstage/plugin-search-react'; -import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; +import { + SearchResultListItemBlueprint, + SearchFilterResultTypeBlueprint, +} from '@backstage/plugin-search-react/alpha'; import { rootRouteRef } from './plugin'; import { SearchClient } from './apis'; @@ -106,6 +109,9 @@ export const searchPage = PageBlueprint.makeWithOverrides({ }, inputs: { items: createExtensionInput([SearchResultListItemBlueprint.dataRefs.item]), + resultTypes: createExtensionInput([ + SearchFilterResultTypeBlueprint.dataRefs.resultType, + ]), }, factory(originalFactory, { config, inputs }) { return originalFactory({ @@ -124,6 +130,10 @@ export const searchPage = PageBlueprint.makeWithOverrides({ ); }; + const resultTypes = inputs.resultTypes.map(item => + item.get(SearchFilterResultTypeBlueprint.dataRefs.resultType), + ); + const Component = () => { const classes = useSearchPageStyles(); const { isMobile } = useSidebarPinState(); @@ -155,7 +165,7 @@ export const searchPage = PageBlueprint.makeWithOverrides({ name: 'Documentation', icon: , }, - ]} + ].concat(resultTypes)} /> {types.includes('techdocs') && (