<SearchResultNext /> -> <SearchResult />, with deprecation warning.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -22,7 +22,7 @@ import { CatalogResultListItem } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
SearchBarNext as SearchBar,
|
||||
SearchFilterNext as SearchFilter,
|
||||
SearchResultNext as SearchResult,
|
||||
SearchResult,
|
||||
DefaultResultListItem,
|
||||
} from '@backstage/plugin-search';
|
||||
|
||||
|
||||
+7
-13
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user