Provide backwards compatibility instead of an upgrade message.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -30,6 +30,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.7.11",
|
||||
"@backstage/catalog-model": "^0.8.0",
|
||||
"@backstage/plugin-catalog-react": "^0.2.0",
|
||||
"@backstage/search-common": "^0.1.1",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/theme": "^0.2.8",
|
||||
|
||||
@@ -23,17 +23,6 @@ export const searchApiRef = createApiRef<SearchApi>({
|
||||
description: 'Used to make requests against the search API',
|
||||
});
|
||||
|
||||
export type Result = {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
owner: string | undefined;
|
||||
kind: string;
|
||||
lifecycle: string | undefined;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type SearchResults = Array<Result>;
|
||||
|
||||
export interface SearchApi {
|
||||
query(query: SearchQuery): Promise<SearchResultSet>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
makeStyles,
|
||||
Typography,
|
||||
Divider,
|
||||
Card,
|
||||
CardHeader,
|
||||
Button,
|
||||
CardContent,
|
||||
Select,
|
||||
Checkbox,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
} from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
filters: {
|
||||
background: 'transparent',
|
||||
boxShadow: '0px 0px 0px 0px',
|
||||
},
|
||||
checkbox: {
|
||||
padding: theme.spacing(0, 1, 0, 1),
|
||||
},
|
||||
dropdown: {
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
export type FiltersState = {
|
||||
selected: string;
|
||||
checked: Array<string>;
|
||||
};
|
||||
|
||||
export type FilterOptions = {
|
||||
kind: Array<string>;
|
||||
lifecycle: Array<string>;
|
||||
};
|
||||
|
||||
type FiltersProps = {
|
||||
filters: FiltersState;
|
||||
filterOptions: FilterOptions;
|
||||
resetFilters: () => void;
|
||||
updateSelected: (filter: string) => void;
|
||||
updateChecked: (filter: string) => void;
|
||||
};
|
||||
|
||||
export const Filters = ({
|
||||
filters,
|
||||
filterOptions,
|
||||
resetFilters,
|
||||
updateSelected,
|
||||
updateChecked,
|
||||
}: FiltersProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card className={classes.filters}>
|
||||
<CardHeader
|
||||
title={<Typography variant="h6">Filters</Typography>}
|
||||
action={
|
||||
<Button color="primary" onClick={() => resetFilters()}>
|
||||
CLEAR ALL
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
{filterOptions.kind.length === 0 && filterOptions.lifecycle.length === 0 && (
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2">
|
||||
Filters cannot be applied to available results
|
||||
</Typography>
|
||||
</CardContent>
|
||||
)}
|
||||
{filterOptions.kind.length > 0 && (
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2">Kind</Typography>
|
||||
<Select
|
||||
id="outlined-select"
|
||||
onChange={(e: React.ChangeEvent<any>) =>
|
||||
updateSelected(e?.target?.value)
|
||||
}
|
||||
variant="outlined"
|
||||
className={classes.dropdown}
|
||||
value={filters.selected}
|
||||
>
|
||||
{filterOptions.kind.map(filter => (
|
||||
<MenuItem
|
||||
selected={filter === ''}
|
||||
dense
|
||||
key={filter}
|
||||
value={filter}
|
||||
>
|
||||
{filter}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</CardContent>
|
||||
)}
|
||||
{filterOptions.lifecycle.length > 0 && (
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2">Lifecycle</Typography>
|
||||
<List disablePadding dense>
|
||||
{filterOptions.lifecycle.map(filter => (
|
||||
<ListItem
|
||||
key={filter}
|
||||
dense
|
||||
button
|
||||
onClick={() => updateChecked(filter)}
|
||||
>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
disableRipple
|
||||
className={classes.checkbox}
|
||||
color="primary"
|
||||
checked={filters.checked.includes(filter)}
|
||||
tabIndex={-1}
|
||||
value={filter}
|
||||
name={filter}
|
||||
/>
|
||||
<ListItemText id={filter} primary={filter} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020 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 FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import { makeStyles, IconButton, Typography } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
filters: {
|
||||
width: '250px',
|
||||
display: 'flex',
|
||||
},
|
||||
icon: {
|
||||
margin: theme.spacing(-1, 0, 0, 0),
|
||||
},
|
||||
}));
|
||||
|
||||
type FiltersButtonProps = {
|
||||
numberOfSelectedFilters: number;
|
||||
handleToggleFilters: () => void;
|
||||
};
|
||||
|
||||
export const FiltersButton = ({
|
||||
numberOfSelectedFilters,
|
||||
handleToggleFilters,
|
||||
}: FiltersButtonProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.filters}>
|
||||
<IconButton
|
||||
className={classes.icon}
|
||||
aria-label="settings"
|
||||
onClick={handleToggleFilters}
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6">
|
||||
Filters ({numberOfSelectedFilters ? numberOfSelectedFilters : 0})
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 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 { FiltersButton } from './FiltersButton';
|
||||
export { Filters } from './Filters';
|
||||
export type { FiltersState } from './Filters';
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 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 { makeStyles } from '@material-ui/core/styles';
|
||||
import { Paper } from '@material-ui/core';
|
||||
import InputBase from '@material-ui/core/InputBase';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import ClearButton from '@material-ui/icons/Clear';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
type SearchBarProps = {
|
||||
searchQuery: string;
|
||||
handleSearch: any;
|
||||
handleClearSearchBar: any;
|
||||
};
|
||||
|
||||
export const SearchBar = ({
|
||||
searchQuery,
|
||||
handleSearch,
|
||||
handleClearSearchBar,
|
||||
}: SearchBarProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={e => handleSearch(e)}
|
||||
className={classes.root}
|
||||
>
|
||||
<IconButton disabled type="submit" aria-label="search">
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder="Search in Backstage"
|
||||
value={searchQuery}
|
||||
onChange={e => handleSearch(e)}
|
||||
inputProps={{ 'aria-label': 'search backstage' }}
|
||||
/>
|
||||
<IconButton aria-label="search" onClick={() => handleClearSearchBar()}>
|
||||
<ClearButton />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2020 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 { Content, Header, Page, useQueryParamState } from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { SearchBar } from './LegacySearchBar';
|
||||
import { SearchResult } from './LegacySearchResult';
|
||||
|
||||
/**
|
||||
* @deprecated This SearchPage, powered directly by the Catalog API, will be
|
||||
* removed from a future release of this plugin.
|
||||
*/
|
||||
export const LegacySearchPage = () => {
|
||||
const [queryString, setQueryString] = useQueryParamState<string>('query');
|
||||
const [searchQuery, setSearchQuery] = useState(queryString ?? '');
|
||||
|
||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
setSearchQuery(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => setSearchQuery(queryString ?? ''), [queryString]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
setQueryString(searchQuery);
|
||||
},
|
||||
200,
|
||||
[searchQuery],
|
||||
);
|
||||
|
||||
const handleClearSearchBar = () => {
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title="Search" />
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchBar
|
||||
handleSearch={handleSearch}
|
||||
handleClearSearchBar={handleClearSearchBar}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResult
|
||||
searchQuery={(queryString ?? '').toLocaleLowerCase('en-US')}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
EmptyState,
|
||||
Link,
|
||||
Progress,
|
||||
Table,
|
||||
TableColumn,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { Divider, Grid, makeStyles, Typography } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { Filters, FiltersButton, FiltersState } from './Filters';
|
||||
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
|
||||
type Result = {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
owner: string | undefined;
|
||||
kind: string;
|
||||
lifecycle: string | undefined;
|
||||
url: string;
|
||||
};
|
||||
type SearchResults = Array<Result>;
|
||||
|
||||
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 TableHeaderProps = {
|
||||
searchQuery?: string;
|
||||
numberOfSelectedFilters: number;
|
||||
numberOfResults: number;
|
||||
handleToggleFilters: () => void;
|
||||
};
|
||||
|
||||
// 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 catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const [showFilters, toggleFilters] = useState(false);
|
||||
const [selectedFilters, setSelectedFilters] = useState<FiltersState>({
|
||||
selected: '',
|
||||
checked: [],
|
||||
});
|
||||
|
||||
const [filteredResults, setFilteredResults] = useState<SearchResults>([]);
|
||||
|
||||
const { loading, error, value: results } = useAsync(async () => {
|
||||
const entities = await catalogApi.getEntities();
|
||||
return entities.items.map((entity: Entity) => ({
|
||||
name: entity.metadata.name,
|
||||
description: entity.metadata.description,
|
||||
owner:
|
||||
typeof entity.spec?.owner === 'string' ? entity.spec?.owner : undefined,
|
||||
kind: entity.kind,
|
||||
lifecycle:
|
||||
typeof entity.spec?.lifecycle === 'string'
|
||||
? entity.spec?.lifecycle
|
||||
: undefined,
|
||||
url: `/catalog/${
|
||||
entity.metadata.namespace?.toLowerCase() || ENTITY_DEFAULT_NAMESPACE
|
||||
}/${entity.kind.toLowerCase()}/${entity.metadata.name}`,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
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 />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching search results. {error.toString()}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (!results || results.length === 0) {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { LegacySearchPage } from './LegacySearchPage';
|
||||
@@ -42,6 +42,11 @@ jest.mock('../SearchContext', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../LegacySearchPage', () => ({
|
||||
...jest.requireActual('../SearchContext'),
|
||||
LegacySearchPage: jest.fn().mockReturnValue('LegacySearchPageMock'),
|
||||
}));
|
||||
|
||||
describe('SearchPage', () => {
|
||||
const origReplaceState = window.history.replaceState;
|
||||
|
||||
@@ -85,11 +90,11 @@ describe('SearchPage', () => {
|
||||
expect(getByText('Route Children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders upgrade error whe no router children are provided', async () => {
|
||||
it('renders legacy search when no router children are provided', async () => {
|
||||
(useOutlet as jest.Mock).mockReturnValueOnce(null);
|
||||
const { getByText } = await renderInTestApp(<SearchPage />);
|
||||
|
||||
expect(getByText('Error: No Search Layout Found')).toBeInTheDocument();
|
||||
expect(getByText('LegacySearchPageMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('replaces window history with expected query parameters', async () => {
|
||||
|
||||
@@ -19,31 +19,7 @@ import qs from 'qs';
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
import { SearchContextProvider, useSearch } from '../SearchContext';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { Content, Header, Page, Link, WarningPanel } from '@backstage/core';
|
||||
|
||||
const UpdateInstructions = () => {
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title="Search" />
|
||||
<Content>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="No Search Layout Found"
|
||||
message={
|
||||
<>
|
||||
As of v0.4.0 of the Backstage Search Plugin, a search layout must
|
||||
be provided by the App. For detailed instructions, check the{' '}
|
||||
<Link to="https://backstage.io/docs/features/search/getting-started">
|
||||
getting started guide
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
import { LegacySearchPage } from '../LegacySearchPage';
|
||||
|
||||
export const UrlUpdater = () => {
|
||||
const { term, types, pageCursor, filters } = useSearch();
|
||||
@@ -87,7 +63,7 @@ export const SearchPage = () => {
|
||||
return (
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<UrlUpdater />
|
||||
{outlet || <UpdateInstructions />}
|
||||
{outlet || <LegacySearchPage />}
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user