delete deprecations

Signed-off-by: Emma Indal <emma.indahl@gmail.com>
This commit is contained in:
Emma Indal
2022-06-30 14:45:17 +02:00
committed by Camila Belo
parent fd1c4910b1
commit f22df2248d
33 changed files with 15 additions and 2272 deletions
@@ -9,7 +9,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common';
import { IndexableResultSet } from '@backstage/plugin-search-common';
import { Knex } from 'knex';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
import { SearchEngine } from '@backstage/plugin-search-common';
import { SearchQuery } from '@backstage/plugin-search-common';
// @public
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { PluginDatabaseManager } from '@backstage/backend-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
import { SearchEngine } from '@backstage/plugin-search-common';
import {
SearchQuery,
IndexableResultSet,
@@ -170,8 +170,6 @@ export type ScheduleTaskParameters = {
scheduledRunner: TaskRunner;
};
export { SearchEngine };
// @public
export class TestPipeline {
execute(): Promise<TestPipelineResult>;
@@ -19,10 +19,11 @@ import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
import {
DocumentCollatorFactory,
DocumentDecoratorFactory,
SearchEngine,
} from '@backstage/plugin-search-common';
import { Readable, Transform } from 'stream';
import { IndexBuilder } from './IndexBuilder';
import { LunrSearchEngine, SearchEngine } from './index';
import { LunrSearchEngine } from './index';
class TestDocumentCollatorFactory implements DocumentCollatorFactory {
readonly type: string = 'anything';
-5
View File
@@ -39,8 +39,3 @@ export * from './indexing';
export * from './test-utils';
export type { ScheduleTaskParameters } from './Scheduler';
/**
* @deprecated Import from @backstage/plugin-search-common instead
*/
export type { SearchEngine } from '@backstage/plugin-search-common';
+1 -1
View File
@@ -9,7 +9,7 @@ import express from 'express';
import { Logger } from 'winston';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
import { SearchEngine } from '@backstage/plugin-search-common';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
@@ -17,10 +17,8 @@
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import {
IndexBuilder,
SearchEngine,
} from '@backstage/plugin-search-backend-node';
import { IndexBuilder } from '@backstage/plugin-search-backend-node';
import { SearchEngine } from '@backstage/plugin-search-common';
import express from 'express';
import request from 'supertest';
+1 -1
View File
@@ -33,7 +33,7 @@ import {
IndexableResultSet,
SearchResultSet,
} from '@backstage/plugin-search-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
import { SearchEngine } from '@backstage/plugin-search-common';
import { AuthorizedSearchEngine } from './AuthorizedSearchEngine';
const jsonObjectSchema: z.ZodSchema<JsonObject> = z.lazy(() => {
+3 -128
View File
@@ -6,143 +6,24 @@
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DefaultResultListItemProps } from '@backstage/plugin-search-react';
import { IconComponent } from '@backstage/core-plugin-api';
import { InputBaseProps } from '@material-ui/core';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react';
import { SearchBarBaseProps as SearchBarBaseProps_2 } from '@backstage/plugin-search-react';
import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react';
import { SearchResultProps } from '@backstage/plugin-search-react';
// @public @deprecated (undocumented)
export const DefaultResultListItem: (
props: DefaultResultListItemProps,
) => JSX.Element;
// @public @deprecated (undocumented)
export type FilterOptions = {
kind: Array<string>;
lifecycle: Array<string>;
};
// @public @deprecated (undocumented)
export const Filters: ({
filters,
filterOptions,
resetFilters,
updateSelected,
updateChecked,
}: FiltersProps) => JSX.Element;
// @public @deprecated (undocumented)
export const FiltersButton: ({
numberOfSelectedFilters,
handleToggleFilters,
}: FiltersButtonProps) => JSX.Element;
// @public @deprecated
export type FiltersButtonProps = {
numberOfSelectedFilters: number;
handleToggleFilters: () => void;
};
// @public @deprecated
export type FiltersProps = {
filters: FiltersState;
filterOptions: FilterOptions;
resetFilters: () => void;
updateSelected: (filter: string) => void;
updateChecked: (filter: string) => void;
};
// @public @deprecated (undocumented)
export type FiltersState = {
selected: string;
checked: Array<string>;
};
import { SearchBarBaseProps } from '@backstage/plugin-search-react';
// @public (undocumented)
export const HomePageSearchBar: ({
...props
}: Partial<Omit<SearchBarBaseProps_2, 'onChange' | 'onSubmit'>>) => JSX.Element;
}: Partial<Omit<SearchBarBaseProps, 'onChange' | 'onSubmit'>>) => JSX.Element;
// @public
export type HomePageSearchBarProps = Partial<
Omit<SearchBarBaseProps_2, 'onChange' | 'onSubmit'>
Omit<SearchBarBaseProps, 'onChange' | 'onSubmit'>
>;
// @public (undocumented)
export const Router: () => JSX.Element;
// @public @deprecated (undocumented)
export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & {
filterSelectedOptions?: boolean;
limitTags?: number;
multiple?: boolean;
};
// @public @deprecated
export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element;
// @public @deprecated
export const SearchBarBase: ({
onChange,
onKeyDown,
onSubmit,
debounceTime,
clearButton,
fullWidth,
value: defaultValue,
inputProps: defaultInputProps,
endAdornment: defaultEndAdornment,
...props
}: SearchBarBaseProps_2) => JSX.Element;
// @public @deprecated
export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
debounceTime?: number;
clearButton?: boolean;
onClear?: () => void;
onSubmit?: () => void;
onChange: (value: string) => void;
};
// @public @deprecated
export type SearchBarProps = Partial<SearchBarBaseProps>;
// @public @deprecated (undocumented)
export const SearchFilter: {
({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element;
Checkbox(
props: Omit<SearchFilterWrapperProps, 'component'> &
SearchFilterComponentProps,
): JSX.Element;
Select(
props: Omit<SearchFilterWrapperProps, 'component'> &
SearchFilterComponentProps,
): JSX.Element;
Autocomplete(props: SearchAutocompleteFilterProps_2): JSX.Element;
};
// @public @deprecated (undocumented)
export type SearchFilterComponentProps = {
className?: string;
name: string;
label?: string;
values?: string[] | ((partial: string) => Promise<string[]>);
defaultValue?: string[] | string | null;
valuesDebounceMs?: number;
};
// @public @deprecated (undocumented)
export type SearchFilterWrapperProps = SearchFilterComponentProps & {
component: (props: SearchFilterComponentProps) => ReactElement;
debug?: boolean;
};
// @public (undocumented)
export const SearchModal: ({
open,
@@ -199,12 +80,6 @@ const searchPlugin: BackstagePlugin<
export { searchPlugin as plugin };
export { searchPlugin };
// @public @deprecated (undocumented)
export const SearchResult: (props: SearchResultProps) => JSX.Element;
// @public @deprecated (undocumented)
export const SearchResultPager: () => JSX.Element;
// @public (undocumented)
export const SearchType: {
(props: SearchTypeProps): JSX.Element;
@@ -1,168 +0,0 @@
/*
* Copyright 2020 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 {
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%',
},
}));
/**
* @public
* @deprecated This type and corresponding component will be removed in a
* future release.
*/
export type FiltersState = {
selected: string;
checked: Array<string>;
};
/**
* @public
* @deprecated This type and corresponding component will be removed in a
* future release.
*/
export type FilterOptions = {
kind: Array<string>;
lifecycle: Array<string>;
};
/**
* Props for {@link Filters}.
*
* @public
* @deprecated This type and corresponding component will be removed in a
* future release.
*/
export type FiltersProps = {
filters: FiltersState;
filterOptions: FilterOptions;
resetFilters: () => void;
updateSelected: (filter: string) => void;
updateChecked: (filter: string) => void;
};
/**
* @public
* @deprecated This component will be removed in a future release. Use
* `SearchFilter` from `@backstage/plugin-search-react` instead.
*/
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>
);
};
@@ -1,67 +0,0 @@
/*
* Copyright 2020 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 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),
},
}));
/**
* Props for {@link FiltersButton}.
*
* @public
* @deprecated This type and corresponding component will be removed in a
* future release.
*/
export type FiltersButtonProps = {
numberOfSelectedFilters: number;
handleToggleFilters: () => void;
};
/**
* @public
* @deprecated See `SearchFilter` in `@backstage/plugin-search-react` instead.
*/
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>
);
};
@@ -1,20 +0,0 @@
/*
* Copyright 2020 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 { FiltersButton } from './FiltersButton';
export type { FiltersButtonProps } from './FiltersButton';
export { Filters } from './Filters';
export type { FilterOptions, FiltersProps, FiltersState } from './Filters';
@@ -1,146 +0,0 @@
/*
* Copyright 2020 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 {
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>
);
};
@@ -1,56 +0,0 @@
/*
* Copyright 2020 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 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>
);
};
@@ -1,19 +0,0 @@
/*
* Copyright 2020 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 { FiltersButton } from './FiltersButton';
export { Filters } from './Filters';
export type { FiltersState } from './Filters';
@@ -1,69 +0,0 @@
/*
* Copyright 2020 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 { 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>
);
};
@@ -1,77 +0,0 @@
/*
* Copyright 2020 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 { Grid } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { SearchBar } from './LegacySearchBar';
import { SearchResult } from './LegacySearchResult';
import {
Content,
Header,
Page,
useQueryParamState,
} from '@backstage/core-components';
/**
* @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>
);
};
@@ -1,297 +0,0 @@
/*
* Copyright 2020 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 { 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/lib/useAsync';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Filters, FiltersButton, FiltersState } from './Filters';
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import {
EmptyState,
Link,
Progress,
Table,
TableColumn,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
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?.toLocaleLowerCase('en-US') ||
DEFAULT_NAMESPACE
}/${entity.kind.toLocaleLowerCase('en-US')}/${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>
</>
);
};
@@ -1,17 +0,0 @@
/*
* Copyright 2021 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 { LegacySearchPage } from './LegacySearchPage';
@@ -1,79 +0,0 @@
/*
* Copyright 2021 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, { useCallback } from 'react';
import { InputBaseProps } from '@material-ui/core';
import {
SearchBarBase as RealSearchBarBase,
useSearch,
} from '@backstage/plugin-search-react';
/**
* Props for {@link SearchBarBase}.
*
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
debounceTime?: number;
clearButton?: boolean;
onClear?: () => void;
onSubmit?: () => void;
onChange: (value: string) => void;
};
/**
* All search boxes exported by the search plugin are based on the <SearchBarBase />,
* and this one is based on the <InputBase /> component from Material UI.
* Recommended if you don't use Search Provider or Search Context.
*
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export const SearchBarBase = RealSearchBarBase;
/**
* Props for {@link SearchBar}.
*
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export type SearchBarProps = Partial<SearchBarBaseProps>;
/**
* Recommended search bar when you use the Search Provider or Search Context.
*
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export const SearchBar = ({ onChange, ...props }: SearchBarProps) => {
const { term, setTerm } = useSearch();
const handleChange = useCallback(
(newValue: string) => {
if (onChange) {
onChange(newValue);
} else {
setTerm(newValue);
}
},
[onChange, setTerm],
);
return <SearchBarBase value={term} onChange={handleChange} {...props} />;
};
@@ -1,18 +0,0 @@
/*
* Copyright 2021 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 { SearchBar, SearchBarBase } from './SearchBar';
export type { SearchBarProps, SearchBarBaseProps } from './SearchBar';
@@ -1,363 +0,0 @@
/*
* Copyright 2021 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 { TestApiProvider } from '@backstage/test-utils';
import { screen, render, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import {
SearchContextProvider,
useSearch,
searchApiRef,
} from '@backstage/plugin-search-react';
import { SearchFilter } from './SearchFilter';
const SearchContextFilterSpy = ({ name }: { name: string }) => {
const { filters } = useSearch();
const value = filters[name];
return (
<span data-testid={`${name}-filter-spy`}>
{Array.isArray(value) ? value.join(',') : value}
</span>
);
};
describe('SearchFilter.Autocomplete', () => {
const query = jest.fn().mockResolvedValue({});
const emptySearchContext = {
term: '',
types: [],
filters: {},
};
const name = 'field';
const values = ['value1', 'value2'];
it('renders as expected', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete name={name} values={values} />
</SearchContextProvider>
</TestApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument();
expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument();
});
it('renders as expected with async values', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete name={name} values={async () => values} />
</SearchContextProvider>
</TestApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument();
expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument();
});
it('does not affect unrelated filter state', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider
initialState={{
...emptySearchContext,
...{ filters: { unrelated: 'value' } },
}}
>
<SearchFilter.Autocomplete name={name} values={values} />
<SearchContextFilterSpy name={name} />
<SearchContextFilterSpy name="unrelated" />
</SearchContextProvider>
</TestApiProvider>,
);
// The spy should show the initial value.
expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent(
'value',
);
// Select a value from the autocomplete filter.
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
await userEvent.click(screen.getByRole('option', { name: values[1] }));
// Wait for the autocomplete filter's value to change.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[1],
);
});
// Unrelated filter spy should maintain the same value.
expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent(
'value',
);
});
describe('single', () => {
it('renders as expected with defaultValue', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete
name={name}
values={values}
defaultValue={values[1]}
/>
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</TestApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await waitFor(() => {
expect(input).toHaveValue(values[1]);
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[1],
);
});
});
it('renders as expected with initial context', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider
initialState={{
...emptySearchContext,
...{ filters: { [name]: values[0] } },
}}
>
<SearchFilter.Autocomplete name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</TestApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await waitFor(() => {
expect(input).toHaveValue(values[0]);
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[0],
);
});
});
it('sets filter state when selecting a value', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</TestApiProvider>,
);
// Select the first option in the autocomplete.
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
await userEvent.click(screen.getByRole('option', { name: values[0] }));
// The value should be present in the context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values[0],
);
});
// Click the "Clear" button to remove the value.
const clearButton = within(autocomplete).getByLabelText('Clear');
await userEvent.click(clearButton);
// That value should have been unset from the context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent('');
});
});
});
describe('multiple', () => {
it('renders as expected with defaultValue', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete
multiple
name={name}
values={values}
defaultValue={values}
/>
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
expect(screen.getByText(values[0])).toBeInTheDocument();
expect(screen.getByText(values[1])).toBeInTheDocument();
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values.join(','),
);
});
});
it('renders as expected with initial context', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider
initialState={{
...emptySearchContext,
...{ filters: { [name]: values } },
}}
>
<SearchFilter.Autocomplete multiple name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
expect(screen.getByText(values[0])).toBeInTheDocument();
expect(screen.getByText(values[1])).toBeInTheDocument();
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values.join(','),
);
});
});
it('respects tag limit configuration', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete
multiple
limitTags={1}
name={name}
values={values}
/>
</SearchContextProvider>
</TestApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
const input = within(autocomplete).getByRole('textbox');
// Select the second value.
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
await userEvent.click(screen.getByRole('option', { name: values[1] }));
await waitFor(() => {
expect(
screen.getByRole('button', { name: values[1] }),
).toBeInTheDocument();
});
// Select the first value.
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(
screen.getByRole('button', { name: values[0] }),
).toBeInTheDocument();
});
// Blur the field and only one tag should be shown with a +1.
input.blur();
expect(
screen.queryByRole('button', { name: values[0] }),
).not.toBeInTheDocument();
expect(screen.getByText('+1')).toBeInTheDocument();
});
it('sets filter state when selecting a value', async () => {
render(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchFilter.Autocomplete multiple name={name} values={values} />
<SearchContextFilterSpy name={name} />
</SearchContextProvider>
</TestApiProvider>,
);
const autocomplete = screen.getByRole('combobox');
// Select both values in the autocomplete.
const input = within(autocomplete).getByRole('textbox');
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await userEvent.click(input);
await waitFor(() => {
screen.getByRole('listbox');
});
await userEvent.click(screen.getByRole('option', { name: values[1] }));
// Both options should be present in the context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(
values.join(','),
);
});
// Click the "Clear" button to remove the value.
const clearButton = within(autocomplete).getByLabelText('Clear');
await userEvent.click(clearButton);
// There should be no content in the filter context.
await waitFor(() => {
expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent('');
});
});
});
});
@@ -1,27 +0,0 @@
/*
* Copyright 2021 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 { SearchFilterComponentProps } from '@backstage/plugin-search-react';
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export type SearchAutocompleteFilterProps = SearchFilterComponentProps & {
filterSelectedOptions?: boolean;
limitTags?: number;
multiple?: boolean;
};
@@ -1,409 +0,0 @@
/*
* Copyright 2021 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 { screen, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchFilter } from './SearchFilter';
import { SearchContextProvider } from '@backstage/plugin-search-react';
import { useApi } from '@backstage/core-plugin-api';
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: jest.fn().mockReturnValue({}),
}));
describe('SearchFilter', () => {
const initialState = {
term: '',
filters: {},
types: [],
};
const label = 'Field';
const name = 'field';
const values = ['value1', 'value2'];
const filters = { unrelated: 'unrelated' };
const query = jest.fn().mockResolvedValue({});
(useApi as jest.Mock).mockReturnValue({ query: query });
afterAll(() => {
jest.resetAllMocks();
});
it('Check that element was rendered and received props', async () => {
const CustomFilter = (props: { name: string }) => <h6>{props.name}</h6>;
render(<SearchFilter name={name} component={CustomFilter} />);
expect(screen.getByRole('heading', { name })).toBeInTheDocument();
});
describe('Checkbox', () => {
it('Renders field name and values when provided as props', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Checkbox label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
expect(
screen.getByRole('checkbox', { name: values[0] }),
).toBeInTheDocument();
expect(
screen.getByRole('checkbox', { name: values[1] }),
).toBeInTheDocument();
});
it('Renders correctly based on filter state', async () => {
render(
<SearchContextProvider
initialState={{
...initialState,
filters: {
[name]: [values[1]],
},
}}
>
<SearchFilter.Checkbox label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
expect(
screen.getByRole('checkbox', { name: values[0] }),
).not.toBeChecked();
expect(screen.getByRole('checkbox', { name: values[1] })).toBeChecked();
});
it('Renders correctly based on defaultValue', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Checkbox
label={label}
name={name}
values={values}
defaultValue={[values[0]]}
/>
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
expect(screen.getByRole('checkbox', { name: values[0] })).toBeChecked();
expect(
screen.getByRole('checkbox', { name: values[1] }),
).not.toBeChecked();
});
it('Checking / unchecking a value sets filter state', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Checkbox label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
const checkBox = screen.getByRole('checkbox', { name: values[0] });
// Check the box.
await userEvent.click(checkBox);
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({ filters: { field: [values[0]] } }),
);
});
// Uncheck the box.
await userEvent.click(checkBox);
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({ filters: {} }),
);
});
});
it('Checking / unchecking a value maintains unrelated filter state', async () => {
render(
<SearchContextProvider initialState={{ ...initialState, filters }}>
<SearchFilter.Checkbox label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
const checkBox = screen.getByRole('checkbox', { name: values[0] });
// Check the box.
await userEvent.click(checkBox);
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({
filters: { ...filters, field: [values[0]] },
}),
);
});
// Uncheck the box.
await userEvent.click(checkBox);
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({ filters }),
);
});
});
});
describe('Select', () => {
it('Renders field name and values when provided as props', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Select label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
expect(
screen.getByRole('option', { name: values[0] }),
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: values[1] }),
).toBeInTheDocument();
});
it('Renders values when provided asynchronously', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Select
label={label}
name={name}
values={async () => values}
/>
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByRole('button')).toBeInTheDocument();
expect(
screen.getByRole('button').getAttribute('aria-disabled'),
).not.toBe('true');
});
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
expect(
screen.getByRole('option', { name: values[0] }),
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: values[1] }),
).toBeInTheDocument();
});
it('Renders correctly based on filter state', async () => {
render(
<SearchContextProvider
initialState={{
...initialState,
filters: {
[name]: values[0],
},
}}
>
<SearchFilter.Select label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
expect(screen.getByRole('option', { name: values[0] })).toHaveAttribute(
'aria-selected',
'true',
);
expect(
screen.getByRole('option', { name: values[1] }),
).not.toHaveAttribute('aria-selected');
expect(screen.getByRole('option', { name: 'All' })).not.toHaveAttribute(
'aria-selected',
);
});
it('Renders correctly based on defaultValue', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Select
name={name}
label={label}
values={values}
defaultValue={values[0]}
/>
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
expect(screen.getByRole('option', { name: values[0] })).toHaveAttribute(
'aria-selected',
'true',
);
expect(
screen.getByRole('option', { name: values[1] }),
).not.toHaveAttribute('aria-selected');
expect(screen.getByRole('option', { name: 'All' })).not.toHaveAttribute(
'aria-selected',
);
});
it('Selecting a value sets filter state', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchFilter.Select label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
const button = screen.getByRole('button');
await userEvent.click(button);
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({
filters: { [name]: values[0] },
}),
);
});
await userEvent.click(button);
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('option', { name: 'All' }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({
filters: {},
}),
);
});
});
it('Selecting a value maintains unrelated filter state', async () => {
render(
<SearchContextProvider
initialState={{
...initialState,
filters,
}}
>
<SearchFilter.Select label={label} name={name} values={values} />
</SearchContextProvider>,
);
await waitFor(() => {
expect(screen.getByText(label)).toBeInTheDocument();
});
const button = screen.getByRole('button');
await userEvent.click(button);
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({
filters: { ...filters, [name]: values[0] },
}),
);
});
await userEvent.click(button);
await waitFor(() => {
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('option', { name: 'All' }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect.objectContaining({ filters }),
);
});
});
});
});
@@ -1,95 +0,0 @@
/*
* Copyright 2021 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, { ReactElement } from 'react';
import {
AutocompleteFilter,
CheckboxFilter,
SearchAutocompleteFilterProps,
SelectFilter,
} from '@backstage/plugin-search-react';
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export type SearchFilterComponentProps = {
className?: string;
name: string;
label?: string;
/**
* Either an array of values directly, or an async function to return a list
* of values to be used in the filter. In the autocomplete filter, the last
* input value is provided as an input to allow values to be filtered. This
* function is debounced and values cached.
*/
values?: string[] | ((partial: string) => Promise<string[]>);
defaultValue?: string[] | string | null;
/**
* Debounce time in milliseconds, used when values is an async callback.
* Defaults to 250ms.
*/
valuesDebounceMs?: number;
};
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export type SearchFilterWrapperProps = SearchFilterComponentProps & {
component: (props: SearchFilterComponentProps) => ReactElement;
debug?: boolean;
};
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
const SearchFilter = ({
component: Element,
...props
}: SearchFilterWrapperProps) => <Element {...props} />;
/**
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
SearchFilter.Checkbox = (
props: Omit<SearchFilterWrapperProps, 'component'> &
SearchFilterComponentProps,
) => <SearchFilter {...props} component={CheckboxFilter} />;
/**
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
SearchFilter.Select = (
props: Omit<SearchFilterWrapperProps, 'component'> &
SearchFilterComponentProps,
) => <SearchFilter {...props} component={SelectFilter} />;
/**
* A control surface for a given filter field name, rendered as an autocomplete
* textfield. A hard-coded list of values may be provided, or an async function
* which returns values may be provided instead.
*
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => (
<SearchFilter {...props} component={AutocompleteFilter} />
);
export { SearchFilter };
@@ -1,22 +0,0 @@
/*
* Copyright 2021 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 { SearchFilter } from './SearchFilter';
export type {
SearchFilterComponentProps,
SearchFilterWrapperProps,
} from './SearchFilter';
export type { SearchAutocompleteFilterProps } from './SearchFilter.Autocomplete';
@@ -27,8 +27,8 @@ import {
import { makeStyles } from '@material-ui/core/styles';
import React, { ComponentType } from 'react';
import { rootRouteRef } from '../../plugin';
import { SearchBar } from '../SearchBar';
import {
SearchBar,
DefaultResultListItem,
searchApiRef,
MockSearchApi,
@@ -23,7 +23,6 @@ import {
useSearch,
} from '@backstage/plugin-search-react';
import { JsonObject } from '@backstage/types';
import { LegacySearchPage } from '../LegacySearchPage';
export const UrlUpdater = () => {
const location = useLocation();
@@ -96,7 +95,7 @@ export const SearchPage = () => {
return (
<SearchContextProvider>
<UrlUpdater />
{outlet || <LegacySearchPage />}
{outlet}
</SearchContextProvider>
);
};
@@ -1,56 +0,0 @@
/*
* Copyright 2021 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 userEvent from '@testing-library/user-event';
import React from 'react';
import { useSearch } from '@backstage/plugin-search-react';
import { SearchResultPager } from './SearchResultPager';
jest.mock('@backstage/plugin-search-react', () => ({
...jest.requireActual('@backstage/plugin-search-react'),
useSearch: jest.fn().mockReturnValue({
result: {},
}),
}));
describe('SearchResultPager', () => {
it('renders pager buttons', async () => {
const fetchNextPage = jest.fn();
const fetchPreviousPage = jest.fn();
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, value: [] },
fetchNextPage,
fetchPreviousPage,
});
const { getByLabelText } = await renderInTestApp(<SearchResultPager />);
await waitFor(() => {
expect(getByLabelText('previous page')).toBeInTheDocument();
});
await userEvent.click(getByLabelText('previous page'));
expect(fetchPreviousPage).toBeCalled();
await waitFor(() => {
expect(getByLabelText('next page')).toBeInTheDocument();
});
await userEvent.click(getByLabelText('next page'));
expect(fetchNextPage).toBeCalled();
});
});
@@ -1,65 +0,0 @@
/*
* Copyright 2021 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 { Button, makeStyles } from '@material-ui/core';
import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
import React from 'react';
import { useSearch } from '@backstage/plugin-search-react';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(2),
margin: theme.spacing(2, 0),
},
}));
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export const SearchResultPager = () => {
const { fetchNextPage, fetchPreviousPage } = useSearch();
const classes = useStyles();
if (!fetchNextPage && !fetchPreviousPage) {
return <></>;
}
return (
<nav arial-label="pagination navigation" className={classes.root}>
<Button
aria-label="previous page"
disabled={!fetchPreviousPage}
onClick={fetchPreviousPage}
startIcon={<ArrowBackIosIcon />}
>
Previous
</Button>
<Button
aria-label="next page"
disabled={!fetchNextPage}
onClick={fetchNextPage}
endIcon={<ArrowForwardIosIcon />}
>
Next
</Button>
</nav>
);
};
@@ -1,17 +0,0 @@
/*
* Copyright 2021 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 { SearchResultPager } from './SearchResultPager';
+1 -21
View File
@@ -20,25 +20,8 @@
* @packageDocumentation
*/
export { Filters, FiltersButton } from './components/Filters';
export type {
FilterOptions,
FiltersState,
FiltersProps,
FiltersButtonProps,
} from './components/Filters';
export type { HomePageSearchBarProps } from './components/HomePageComponent';
export { SearchBar, SearchBarBase } from './components/SearchBar';
export type {
SearchBarBaseProps,
SearchBarProps,
} from './components/SearchBar';
export { SearchFilter } from './components/SearchFilter';
export type {
SearchAutocompleteFilterProps,
SearchFilterComponentProps,
SearchFilterWrapperProps,
} from './components/SearchFilter';
export {
SearchModal,
SearchModalProvider,
@@ -51,7 +34,6 @@ export type {
SearchModalValue,
} from './components/SearchModal';
export { SearchPage as Router } from './components/SearchPage';
export { SearchResultPager } from './components/SearchResultPager';
export { SearchType } from './components/SearchType';
export type {
SearchTypeAccordionProps,
@@ -63,11 +45,9 @@ export type { SidebarSearchProps } from './components/SidebarSearch';
export type { SidebarSearchModalProps } from './components/SidebarSearchModal';
export {
DefaultResultListItem,
HomePageSearchBar,
SearchPage,
searchPlugin as plugin,
searchPlugin,
SearchResult,
SidebarSearchModal,
} from './plugin';
+1 -17
View File
@@ -15,11 +15,7 @@
*/
import { SearchClient } from './apis';
import {
searchApiRef,
SearchResult as RealSearchResult,
DefaultResultListItem as RealDefaultResultListItem,
} from '@backstage/plugin-search-react';
import { searchApiRef } from '@backstage/plugin-search-react';
import {
createApiFactory,
createPlugin,
@@ -64,12 +60,6 @@ export const SearchPage = searchPlugin.provide(
}),
);
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export const SearchResult = RealSearchResult;
/**
* @public
*/
@@ -85,12 +75,6 @@ export const SidebarSearchModal = searchPlugin.provide(
}),
);
/**
* @public
* @deprecated Import from `@backstage/plugin-search-react` instead.
*/
export const DefaultResultListItem = RealDefaultResultListItem;
/**
* @public
*/