feat: implement search

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-14 14:27:47 +02:00
parent 1612837c8e
commit 8f8519d94e
8 changed files with 310 additions and 31 deletions
+21 -30
View File
@@ -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 = {}> = T & {
rank?: number;
result?: SearchDocument;
} & Omit<ListItemProps, 'button'>;
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<TConfig>) {
@@ -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;
}
@@ -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 () => () => <div>Hello</div>,
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 } }) =>
() =>
<div>noTrack: {String(noTrack)}</div>,
},
});
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 (
<div>
{items.map((item, i) => (
<item.component key={i} />
))}
</div>
);
},
});
},
});
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();
});
});
@@ -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<SearchResultItemExtensionComponent>;
/**
* 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) => (
<ExtensionBoundary node={node}>
<SearchResultListItemExtension
rank={props.rank}
result={props.result}
noTrack={config.noTrack}
>
<ExtensionComponent {...props} />
</SearchResultListItemExtension>
</ExtensionBoundary>
),
});
},
});
@@ -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';
@@ -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 = {}> = T & {
rank?: number;
result?: SearchDocument;
} & Omit<ListItemProps, 'button'>;
/** @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' });
+15
View File
@@ -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);
};