From 8f8519d94e227c7d0cab59c4cdddee5499cee283 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 14 Aug 2024 14:27:47 +0200 Subject: [PATCH] feat: implement search Signed-off-by: blam --- .../catalog-react/src/alpha/extensions.tsx | 2 +- plugins/search-react/src/alpha.tsx | 51 +++----- .../SearchResultListItemBlueprint.test.tsx | 123 ++++++++++++++++++ .../SearchResultListItemBlueprint.tsx | 81 ++++++++++++ plugins/search-react/src/blueprints/index.ts | 25 ++++ plugins/search-react/src/blueprints/types.ts | 42 ++++++ plugins/search-react/src/setupTests.ts | 15 +++ yarn.lock | 2 + 8 files changed, 310 insertions(+), 31 deletions(-) create mode 100644 plugins/search-react/src/blueprints/SearchResultListItemBlueprint.test.tsx create mode 100644 plugins/search-react/src/blueprints/SearchResultListItemBlueprint.tsx create mode 100644 plugins/search-react/src/blueprints/index.ts create mode 100644 plugins/search-react/src/blueprints/types.ts diff --git a/plugins/catalog-react/src/alpha/extensions.tsx b/plugins/catalog-react/src/alpha/extensions.tsx index fa91494bd4..e527edc355 100644 --- a/plugins/catalog-react/src/alpha/extensions.tsx +++ b/plugins/catalog-react/src/alpha/extensions.tsx @@ -36,7 +36,7 @@ export * from '../translation'; /** * @alpha - * @deprecated use `dataRefs` on the blueprints instead + * @deprecated use `dataRefs` property on either {@link EntityCardBlueprint} or {@link EntityContentBlueprint} instead */ export const catalogExtensionData = { entityContentTitle: createExtensionDataRef().with({ diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index a22eaf4244..53bf76f775 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -15,39 +15,25 @@ */ import React, { lazy } from 'react'; - -import { ListItemProps } from '@material-ui/core/ListItem'; - import { ExtensionBoundary, PortableSchema, createExtension, - createExtensionDataRef, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; -import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; - import { SearchResultListItemExtension } from './extensions'; +import { + SearchResultItemExtensionComponent, + SearchResultItemExtensionPredicate, + searchResultListItemDataRef, +} from './blueprints/types'; -/** @alpha */ -export type BaseSearchResultListItemProps = T & { - rank?: number; - result?: SearchDocument; -} & Omit; +export * from './blueprints'; -/** @alpha */ -export type SearchResultItemExtensionComponent = < - P extends BaseSearchResultListItemProps, ->( - props: P, -) => JSX.Element | null; - -/** @alpha */ -export type SearchResultItemExtensionPredicate = ( - result: SearchResult, -) => boolean; - -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export type SearchResultItemExtensionOptions< TConfig extends { noTrack?: boolean }, > = { @@ -80,7 +66,12 @@ export type SearchResultItemExtensionOptions< predicate?: SearchResultItemExtensionPredicate; }; -/** @alpha */ +/** + * Creates items for the search result list. + * + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean }, >(options: SearchResultItemExtensionOptions) { @@ -132,10 +123,10 @@ export function createSearchResultListItemExtension< }); } -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export namespace createSearchResultListItemExtension { - export const itemDataRef = createExtensionDataRef<{ - predicate?: SearchResultItemExtensionPredicate; - component: SearchResultItemExtensionComponent; - }>().with({ id: 'search.search-result-list-item.item' }); + export const itemDataRef = searchResultListItemDataRef; } diff --git a/plugins/search-react/src/blueprints/SearchResultListItemBlueprint.test.tsx b/plugins/search-react/src/blueprints/SearchResultListItemBlueprint.test.tsx new file mode 100644 index 0000000000..4f3b7501b4 --- /dev/null +++ b/plugins/search-react/src/blueprints/SearchResultListItemBlueprint.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2024 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 { SearchResultListItemBlueprint } from './SearchResultListItemBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + PageBlueprint, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { searchResultListItemDataRef } from './types'; +import _ from 'lodash'; + +describe('SearchResultListItemBlueprint', () => { + it('should return an extension with sane defaults', () => { + const extension = SearchResultListItemBlueprint.make({ + name: 'test', + params: { + component: async () => () =>
Hello
, + predicate: () => true, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:search", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "noTrack": { + "default": false, + "type": "boolean", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "search-result-list-item", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('defaults and passes on config properly', async () => { + const extension = SearchResultListItemBlueprint.make({ + name: 'test', + params: { + component: + async ({ config: { noTrack } }) => + () => +
noTrack: {String(noTrack)}
, + }, + }); + + const mockSearchPage = PageBlueprint.makeWithOverrides({ + namespace: 'search', + inputs: { + items: createExtensionInput([searchResultListItemDataRef]), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/', + loader: async () => { + const items = inputs.items.map(i => + i.get(searchResultListItemDataRef), + ); + return ( +
+ {items.map((item, i) => ( + + ))} +
+ ); + }, + }); + }, + }); + + await expect( + createExtensionTester(mockSearchPage) + .add(extension) + .render() + .findByText('noTrack: false'), + ).resolves.toBeInTheDocument(); + + await expect( + createExtensionTester(mockSearchPage) + .add(extension, { config: { noTrack: true } }) + .render() + .findByText('noTrack: true'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/blueprints/SearchResultListItemBlueprint.tsx b/plugins/search-react/src/blueprints/SearchResultListItemBlueprint.tsx new file mode 100644 index 0000000000..bea52b7f45 --- /dev/null +++ b/plugins/search-react/src/blueprints/SearchResultListItemBlueprint.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2024 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, { lazy } from 'react'; +import { + createExtensionBlueprint, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { + SearchResultItemExtensionComponent, + SearchResultItemExtensionPredicate, + searchResultListItemDataRef, +} from './types'; +import { + SearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '../extensions'; + +export interface SearchResultListItemBlueprintParams { + /** + * The extension component. + */ + component: (options: { + config: { noTrack?: boolean }; + }) => Promise; + /** + * When an extension defines a predicate, it returns true if the result should be rendered by that extension. + * Defaults to a predicate that returns true, which means it renders all sorts of results. + */ + predicate?: SearchResultItemExtensionPredicate; +} + +export const SearchResultListItemBlueprint = createExtensionBlueprint({ + kind: 'search-result-list-item', + attachTo: { + id: 'page:search', + input: 'items', + }, + config: { + schema: { + noTrack: z => z.boolean().default(false), + }, + }, + output: [searchResultListItemDataRef], + dataRefs: { + item: searchResultListItemDataRef, + }, + *factory(params: SearchResultListItemBlueprintParams, { config, node }) { + const ExtensionComponent = lazy(() => + params.component({ config }).then(component => ({ default: component })), + ); + + yield searchResultListItemDataRef({ + predicate: params.predicate, + component: (props: SearchResultListItemExtensionProps) => ( + + + + + + ), + }); + }, +}); diff --git a/plugins/search-react/src/blueprints/index.ts b/plugins/search-react/src/blueprints/index.ts new file mode 100644 index 0000000000..b9bbf1012e --- /dev/null +++ b/plugins/search-react/src/blueprints/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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 { + SearchResultListItemBlueprint, + type SearchResultListItemBlueprintParams, +} from './SearchResultListItemBlueprint'; +export { + type BaseSearchResultListItemProps, + type SearchResultItemExtensionComponent, + type SearchResultItemExtensionPredicate, +} from './types'; diff --git a/plugins/search-react/src/blueprints/types.ts b/plugins/search-react/src/blueprints/types.ts new file mode 100644 index 0000000000..abfed4aba6 --- /dev/null +++ b/plugins/search-react/src/blueprints/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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 { ListItemProps } from '@material-ui/core/ListItem'; +import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; +import { createExtensionDataRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export type BaseSearchResultListItemProps = T & { + rank?: number; + result?: SearchDocument; +} & Omit; + +/** @alpha */ +export type SearchResultItemExtensionComponent = < + P extends BaseSearchResultListItemProps, +>( + props: P, +) => JSX.Element | null; + +/** @alpha */ +export type SearchResultItemExtensionPredicate = ( + result: SearchResult, +) => boolean; + +export const searchResultListItemDataRef = createExtensionDataRef<{ + predicate?: SearchResultItemExtensionPredicate; + component: SearchResultItemExtensionComponent; +}>().with({ id: 'search.search-result-list-item.item' }); diff --git a/plugins/search-react/src/setupTests.ts b/plugins/search-react/src/setupTests.ts index 992b60d3a4..c47fc7a427 100644 --- a/plugins/search-react/src/setupTests.ts +++ b/plugins/search-react/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/yarn.lock b/yarn.lock index 9c292f5903..dea942a3b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5921,6 +5921,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" @@ -6001,6 +6002,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^"