<SearchResultNext /> -> <SearchResult />, with deprecation warning.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-06-04 17:50:53 +02:00
parent ab548091ed
commit 42b3615b04
9 changed files with 42 additions and 318 deletions
@@ -17,7 +17,7 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { SearchResultNext } from './SearchResultNext';
import { SearchResult } from './SearchResult';
import { useSearch } from '../SearchContext';
jest.mock('../SearchContext', () => ({
@@ -27,15 +27,13 @@ jest.mock('../SearchContext', () => ({
}),
}));
describe('SearchResultNext', () => {
describe('SearchResult', () => {
it('Progress rendered on Loading state', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: true },
});
const { getByRole } = render(
<SearchResultNext>{() => <></>}</SearchResultNext>,
);
const { getByRole } = render(<SearchResult>{() => <></>}</SearchResult>);
await waitFor(() => {
expect(getByRole('progressbar')).toBeInTheDocument();
@@ -48,9 +46,7 @@ describe('SearchResultNext', () => {
result: { loading: false, error },
});
const { getByRole } = render(
<SearchResultNext>{() => <></>}</SearchResultNext>,
);
const { getByRole } = render(<SearchResult>{() => <></>}</SearchResult>);
await waitFor(() => {
expect(getByRole('alert')).toHaveTextContent(
@@ -64,9 +60,7 @@ describe('SearchResultNext', () => {
result: { loading: false, error: '', value: undefined },
});
const { getByRole } = render(
<SearchResultNext>{() => <></>}</SearchResultNext>,
);
const { getByRole } = render(<SearchResult>{() => <></>}</SearchResult>);
await waitFor(() => {
expect(
@@ -81,12 +75,12 @@ describe('SearchResultNext', () => {
});
render(
<SearchResultNext>
<SearchResult>
{({ results }) => {
expect(results).toEqual([]);
return <></>;
}}
</SearchResultNext>,
</SearchResult>,
);
});
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Spotify AB
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,162 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EmptyState,
Link,
Progress,
Table,
TableColumn,
useApi,
} from '@backstage/core';
import { Divider, Grid, makeStyles, Typography } from '@material-ui/core';
import React from 'react';
import { EmptyState, Progress } from '@backstage/core';
import { SearchResult } from '@backstage/search-common';
import { Alert } from '@material-ui/lab';
import React, { useEffect, useState } from 'react';
import { useAsync } from 'react-use';
import { Result, SearchResults, searchApiRef } from '../../apis';
import { Filters, FiltersButton, FiltersState } from '../Filters';
import { useSearch } from '../SearchContext';
const useStyles = makeStyles(theme => ({
searchQuery: {
color: theme.palette.text.primary,
background: theme.palette.background.default,
borderRadius: '10%',
},
tableHeader: {
margin: theme.spacing(1, 0, 0, 0),
display: 'flex',
},
divider: {
width: '1px',
margin: theme.spacing(0, 2),
padding: theme.spacing(2, 0),
},
}));
type SearchResultProps = {
searchQuery?: string;
type Props = {
children: (results: { results: SearchResult[] }) => JSX.Element;
};
type TableHeaderProps = {
searchQuery?: string;
numberOfSelectedFilters: number;
numberOfResults: number;
handleToggleFilters: () => void;
};
const SearchResultComponent = ({ children }: Props) => {
const {
result: { loading, error, value },
} = useSearch();
// TODO: move out column to make the search result component more generic
const columns: TableColumn[] = [
{
title: 'Name',
field: 'name',
highlight: true,
render: (result: Partial<Result>) => (
<Link to={result.url || ''}>{result.name}</Link>
),
},
{
title: 'Description',
field: 'description',
},
{
title: 'Owner',
field: 'owner',
},
{
title: 'Kind',
field: 'kind',
},
{
title: 'LifeCycle',
field: 'lifecycle',
},
];
const TableHeader = ({
searchQuery,
numberOfSelectedFilters,
numberOfResults,
handleToggleFilters,
}: TableHeaderProps) => {
const classes = useStyles();
return (
<div className={classes.tableHeader}>
<FiltersButton
numberOfSelectedFilters={numberOfSelectedFilters}
handleToggleFilters={handleToggleFilters}
/>
<Divider className={classes.divider} orientation="vertical" />
<Grid item xs={12}>
{searchQuery ? (
<Typography variant="h6">
{`${numberOfResults} `}
{numberOfResults > 1 ? `results for ` : `result for `}
<span className={classes.searchQuery}>"{searchQuery}"</span>{' '}
</Typography>
) : (
<Typography variant="h6">{`${numberOfResults} results`}</Typography>
)}
</Grid>
</div>
);
};
export const SearchResult = ({ searchQuery }: SearchResultProps) => {
const searchApi = useApi(searchApiRef);
const [showFilters, toggleFilters] = useState(false);
const [selectedFilters, setSelectedFilters] = useState<FiltersState>({
selected: '',
checked: [],
});
const [filteredResults, setFilteredResults] = useState<SearchResults>([]);
const { loading, error, value: results } = useAsync(() => {
return searchApi.getSearchResult();
}, []);
useEffect(() => {
if (results) {
let withFilters = results;
// apply filters
// filter on selected
if (selectedFilters.selected !== '') {
withFilters = results.filter((result: Result) =>
selectedFilters.selected.includes(result.kind),
);
}
// filter on checked
if (selectedFilters.checked.length > 0) {
withFilters = withFilters.filter(
(result: Result) =>
result.lifecycle &&
selectedFilters.checked.includes(result.lifecycle),
);
}
// filter on searchQuery
if (searchQuery) {
withFilters = withFilters.filter(
(result: Result) =>
result.name?.toLocaleLowerCase('en-US').includes(searchQuery) ||
result.name
?.toLocaleLowerCase('en-US')
.includes(searchQuery.split(' ').join('-')) ||
result.description
?.toLocaleLowerCase('en-US')
.includes(searchQuery),
);
}
setFilteredResults(withFilters);
}
}, [selectedFilters, searchQuery, results]);
if (loading) {
return <Progress />;
}
@@ -179,88 +40,12 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => {
</Alert>
);
}
if (!results || results.length === 0) {
if (!value) {
return <EmptyState missing="data" title="Sorry, no results were found" />;
}
const resetFilters = () => {
setSelectedFilters({
selected: '',
checked: [],
});
};
const updateSelected = (filter: string) => {
setSelectedFilters(prevState => ({
...prevState,
selected: filter,
}));
};
const updateChecked = (filter: string) => {
if (selectedFilters.checked.includes(filter)) {
setSelectedFilters(prevState => ({
...prevState,
checked: prevState.checked.filter(item => item !== filter),
}));
return;
}
setSelectedFilters(prevState => ({
...prevState,
checked: [...prevState.checked, filter],
}));
};
const filterOptions = results.reduce(
(acc, curr) => {
if (curr.kind && acc.kind.indexOf(curr.kind) < 0) {
acc.kind.push(curr.kind);
}
if (curr.lifecycle && acc.lifecycle.indexOf(curr.lifecycle) < 0) {
acc.lifecycle.push(curr.lifecycle);
}
return acc;
},
{
kind: [] as Array<string>,
lifecycle: [] as Array<string>,
},
);
return (
<>
<Grid container>
{showFilters && (
<Grid item xs={3}>
<Filters
filters={selectedFilters}
filterOptions={filterOptions}
resetFilters={resetFilters}
updateSelected={updateSelected}
updateChecked={updateChecked}
/>
</Grid>
)}
<Grid item xs={showFilters ? 9 : 12}>
<Table
options={{ paging: true, pageSize: 20, search: false }}
data={filteredResults}
columns={columns}
title={
<TableHeader
searchQuery={searchQuery}
numberOfResults={filteredResults.length}
numberOfSelectedFilters={
(selectedFilters.selected !== '' ? 1 : 0) +
selectedFilters.checked.length
}
handleToggleFilters={() => toggleFilters(!showFilters)}
/>
}
/>
</Grid>
</Grid>
</>
);
return children({ results: value.results });
};
export { SearchResultComponent as SearchResult };
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Spotify AB
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,49 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { EmptyState, Progress } from '@backstage/core';
import { SearchResult } from '@backstage/search-common';
import { Alert } from '@material-ui/lab';
import { useSearch } from '../SearchContext';
type Props = {
children: (results: { results: SearchResult[] }) => JSX.Element;
};
export const SearchResultNext = ({ children }: Props) => {
const {
result: { loading, error, value },
} = useSearch();
if (loading) {
return <Progress />;
}
if (error) {
return (
<Alert severity="error">
Error encountered while fetching search results. {error.toString()}
</Alert>
);
}
if (!value) {
return <EmptyState missing="data" title="Sorry, no results were found" />;
}
return children({ results: value.results });
};
@@ -1,17 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { SearchResultNext } from './SearchResultNext';
-1
View File
@@ -20,7 +20,6 @@ export * from './SearchBar';
export * from './SearchBarNext';
export * from './SearchPage';
export * from './SearchResult';
export * from './SearchResultNext';
export * from './DefaultResultListItem';
export * from './SidebarSearch';
export * from './SearchContext';
+1 -2
View File
@@ -21,7 +21,7 @@ export {
SearchPage,
SearchPageNext,
SearchBarNext,
SearchResultNext,
SearchResult,
DefaultResultListItem,
} from './plugin';
export {
@@ -32,7 +32,6 @@ export {
useSearch,
SearchPage as Router,
SearchFilterNext,
SearchResult,
SidebarSearch,
} from './components';
export type { FiltersState } from './components';
+15 -2
View File
@@ -80,11 +80,24 @@ export const SearchBarNext = searchPlugin.provide(
}),
);
export const SearchResult = searchPlugin.provide(
createComponentExtension({
component: {
lazy: () => import('./components/SearchResult').then(m => m.SearchResult),
},
}),
);
/**
* @deprecated This component was used for rapid prototyping of the Backstage
* Search platform. Now that the API has stabilized, you should use the
* <SearchResult /> component instead. This component will be removed in an
* upcoming release.
*/
export const SearchResultNext = searchPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/SearchResultNext').then(m => m.SearchResultNext),
lazy: () => import('./components/SearchResult').then(m => m.SearchResult),
},
}),
);