add blueprint extension for result type filter in search plugin

Signed-off-by: Mark Dunphy <markd@spotify.com>
This commit is contained in:
Mark Dunphy
2025-01-07 17:17:13 -05:00
parent aaad211d68
commit d90f71068a
7 changed files with 271 additions and 23 deletions
+37
View File
@@ -17,6 +17,43 @@ export type BaseSearchResultListItemProps<T = {}> = T & {
result?: SearchDocument;
} & Omit<ListItemProps, 'button'>;
// @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,
@@ -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: <div>Hello</div>,
},
});
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: <div>Hello</div>,
},
});
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 (
<div>
{resultTypes.map((t, i) => (
<div key={i}>{t.name}</div>
))}
</div>
);
},
});
},
});
await expect(
renderInTestApp(
createExtensionTester(searchPage).add(extension).reactElement(),
).findByText('Test Result Type'),
).resolves.toBeInTheDocument();
});
});
@@ -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,
});
},
});
@@ -23,3 +23,8 @@ export {
type SearchResultItemExtensionComponent,
type SearchResultItemExtensionPredicate,
} from './types';
export {
SearchFilterResultTypeBlueprint,
type SearchFilterResultTypeBlueprintParams,
} from './SearchFilterResultTypeBlueprint';
@@ -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' });
+51 -21
View File
@@ -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<undefined>;
},
'core.nav-item.target',
{}
>;
inputs: {};
params: {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
};
}>;
'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<AnyRouteRefParams> | undefined;
};
}>;
'nav-item:search': ExtensionDefinition<{
kind: 'nav-item';
name: undefined;
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
{
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
},
'core.nav-item.target',
{}
>;
inputs: {};
params: {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
};
}>;
}
>;
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;
+12 -2
View File
@@ -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: <DocsIcon />,
},
]}
].concat(resultTypes)}
/>
<Paper className={classes.filters}>
{types.includes('techdocs') && (