Move SearchResult from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search

Signed-off-by: Anders Näsman <andersn@spotify.com>
This commit is contained in:
Anders Näsman
2022-06-01 16:37:19 +02:00
committed by Eric Peterson
parent d7044784f0
commit 01abb3bc7e
8 changed files with 225 additions and 9 deletions
@@ -0,0 +1,103 @@
/*
* 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 { Link } from '@backstage/core-components';
import { List, ListItem } from '@material-ui/core';
import React, { ComponentType } from 'react';
import { MemoryRouter } from 'react-router';
import { DefaultResultListItem } from '@backstage/plugin-search';
import { searchApiRef, MockSearchApi } from '../../api';
import { SearchContextProvider } from '../../context';
import { SearchResult } from './SearchResult';
import { TestApiProvider } from '@backstage/test-utils';
const mockResults = {
results: [
{
type: 'custom-result-item',
document: {
location: 'search/search-result-1',
title: 'Search Result 1',
text: 'some text from the search result',
},
},
{
type: 'no-custom-result-item',
document: {
location: 'search/search-result-2',
title: 'Search Result 2',
text: 'some text from the search result',
},
},
{
type: 'no-custom-result-item',
document: {
location: 'search/search-result-3',
title: 'Search Result 3',
text: 'some text from the search result',
},
},
],
};
export default {
title: 'Plugins/Search/SearchResult',
component: SearchResult,
decorators: [
(Story: ComponentType<{}>) => (
<MemoryRouter>
<TestApiProvider
apis={[[searchApiRef, new MockSearchApi(mockResults)]]}
>
<SearchContextProvider>
<Story />
</SearchContextProvider>
</TestApiProvider>
</MemoryRouter>
),
],
};
export const Default = () => {
return (
<SearchResult>
{({ results }) => (
<List>
{results.map(({ type, document }) => {
switch (type) {
case 'custom-result-item':
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
default:
return (
<ListItem>
<Link to={document.location}>
{document.title} - {document.text}
</Link>
</ListItem>
);
}
})}
</List>
)}
</SearchResult>
);
};
@@ -0,0 +1,125 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { useSearch } from '../../context';
import { SearchResult } from './SearchResult';
jest.mock('../../context', () => ({
...jest.requireActual('../../context'),
useSearch: jest.fn().mockReturnValue({
result: {},
}),
}));
describe('SearchResult', () => {
it('Progress rendered on Loading state', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: true },
});
const { getByRole } = await renderInTestApp(
<SearchResult>{() => <></>}</SearchResult>,
);
await waitFor(() => {
expect(getByRole('progressbar')).toBeInTheDocument();
});
});
it('Alert rendered on Error state', async () => {
const error = new Error('some error');
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, error },
});
const { getByRole } = await renderInTestApp(
<SearchResult>{() => <></>}</SearchResult>,
);
await waitFor(() => {
expect(getByRole('alert')).toHaveTextContent(
new RegExp(`Error encountered while fetching search results.*${error}`),
);
});
});
it('On no result value state', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, error: '', value: undefined },
});
const { getByRole } = await renderInTestApp(
<SearchResult>{() => <></>}</SearchResult>,
);
await waitFor(() => {
expect(
getByRole('heading', { name: 'Sorry, no results were found' }),
).toBeInTheDocument();
});
});
it('On empty result value state', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, error: '', value: { results: [] } },
});
const { getByRole } = await renderInTestApp(
<SearchResult>{() => <></>}</SearchResult>,
);
await waitFor(() => {
expect(
getByRole('heading', { name: 'Sorry, no results were found' }),
).toBeInTheDocument();
});
});
it('Calls children with results set to result.value', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: {
loading: false,
error: '',
value: {
totalCount: 1,
results: [
{
type: 'some-type',
document: {
title: 'some-title',
text: 'some-text',
location: 'some-location',
},
},
],
},
},
});
const { getByText } = await renderInTestApp(
<SearchResult>
{({ results }) => {
return <>Results {results.length}</>;
}}
</SearchResult>,
);
expect(getByText('Results 1')).toBeInTheDocument();
});
});
@@ -0,0 +1,60 @@
/*
* 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 {
EmptyState,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { SearchResult } from '@backstage/plugin-search-common';
import React from 'react';
import { useSearch } from '../../context';
/**
* @public
*/
export type SearchResultProps = {
children: (results: { results: SearchResult[] }) => JSX.Element;
};
/**
* @public
*/
export const SearchResultComponent = ({ children }: SearchResultProps) => {
const {
result: { loading, error, value },
} = useSearch();
if (loading) {
return <Progress />;
}
if (error) {
return (
<ResponseErrorPanel
title="Error encountered while fetching search results"
error={error}
/>
);
}
if (!value?.results.length) {
return <EmptyState missing="data" title="Sorry, no results were found" />;
}
return <>{children({ results: value.results })}</>;
};
export { SearchResultComponent as SearchResult };
@@ -0,0 +1,18 @@
/*
* 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 { SearchResult } from './SearchResult';
export type { SearchResultProps } from './SearchResult';
@@ -16,3 +16,4 @@
export * from './HighlightedSearchResultText';
export * from './SearchFilter';
export * from './SearchResult';