From d9cd644a1fe584e50154da1acf593925c6a67cdd Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Tue, 18 May 2021 19:38:52 +0200 Subject: [PATCH 01/56] search context Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../SearchContext/SearchContext.tsx | 68 +++++++++++++++++++ .../src/components/SearchContext/index.tsx | 17 +++++ 2 files changed, 85 insertions(+) create mode 100644 plugins/search/src/components/SearchContext/SearchContext.tsx create mode 100644 plugins/search/src/components/SearchContext/index.tsx diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx new file mode 100644 index 0000000000..ca1a82cde0 --- /dev/null +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -0,0 +1,68 @@ +/* + * 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, { + PropsWithChildren, + createContext, + useContext, + useState, +} from 'react'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core'; +import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { searchApiRef } from '../../apis'; +import { AsyncState } from 'react-use/lib/useAsync'; + +type SearchContextValue = { + resultState: AsyncState; + queryState: SearchQuery; + setQueryState: React.Dispatch>; +}; + +const SearchContext = createContext({} as SearchContextValue); + +export const SearchContextProvider = ({ + initialState = { + term: '', + pageCursor: '', + types: ['*'], + }, + children, +}: PropsWithChildren<{ initialState?: any }>) => { + const searchApi = useApi(searchApiRef); + const [queryState, setQueryState] = useState(initialState); + + const resultState = useAsync( + () => + searchApi._alphaPerformSearch({ + term: queryState.term, + pageCursor: queryState.pageCursor, + }), + [queryState.term], + ); + + const value: SearchContextValue = { resultState, queryState, setQueryState }; + + return ; +}; + +export const useSearch = () => { + const context = useContext(SearchContext); + if (context === undefined) { + throw new Error('useSearch must be used within a SearchContextProvider'); + } + return context; +}; diff --git a/plugins/search/src/components/SearchContext/index.tsx b/plugins/search/src/components/SearchContext/index.tsx new file mode 100644 index 0000000000..b45c169879 --- /dev/null +++ b/plugins/search/src/components/SearchContext/index.tsx @@ -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 { SearchContextProvider, useSearch } from './SearchContext'; From 0f7799795348f3e3c1bdfa8120de5a02ae461a5f Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Tue, 18 May 2021 19:41:10 +0200 Subject: [PATCH 02/56] wip new search page components Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../SearchBarNext/SearchBarNext.tsx | 89 +++++++++++++++++++ .../src/components/SearchBarNext/index.tsx | 17 ++++ .../SearchResultNext/SearchResultNext.tsx | 67 ++++++++++++++ .../src/components/SearchResultNext/index.tsx | 17 ++++ 4 files changed, 190 insertions(+) create mode 100644 plugins/search/src/components/SearchBarNext/SearchBarNext.tsx create mode 100644 plugins/search/src/components/SearchBarNext/index.tsx create mode 100644 plugins/search/src/components/SearchResultNext/SearchResultNext.tsx create mode 100644 plugins/search/src/components/SearchResultNext/index.tsx diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx new file mode 100644 index 0000000000..1e4a59ac8b --- /dev/null +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useEffect } from 'react'; +import { useDebounce } from 'react-use'; +import { useQueryParamState } from '@backstage/core'; +import { Paper, InputBase, IconButton, makeStyles } from '@material-ui/core'; +import SearchIcon from '@material-ui/icons/Search'; +import ClearButton from '@material-ui/icons/Clear'; +import { useSearch } from '../SearchContext'; + +const useStyles = makeStyles(() => ({ + root: { + display: 'flex', + alignItems: 'center', + }, + input: { + flex: 1, + }, +})); + +export const SearchBarNext = () => { + const classes = useStyles(); + const { + queryState: { term }, + setQueryState, + } = useSearch(); + + const [queryString, setQueryString] = useQueryParamState('query'); + + useEffect(() => { + setQueryState({ term: queryString ?? '', pageCursor: '' }); + }, [queryString, setQueryState]); + + useDebounce( + () => { + setQueryString(term); + }, + 200, + [term], + ); + + const handleSearch = (event: React.ChangeEvent | React.FormEvent) => { + event.preventDefault(); + setQueryState({ + term: (event.target as HTMLInputElement).value, + pageCursor: '', + }); + }; + + const handleClearSearchBar = () => { + setQueryState({ term: '', pageCursor: '' }); + }; + + return ( + handleSearch(e)} + className={classes.root} + > + + + + handleSearch(e)} + inputProps={{ 'aria-label': 'search backstage' }} + /> + handleClearSearchBar()}> + + + + ); +}; diff --git a/plugins/search/src/components/SearchBarNext/index.tsx b/plugins/search/src/components/SearchBarNext/index.tsx new file mode 100644 index 0000000000..20ba7d9c55 --- /dev/null +++ b/plugins/search/src/components/SearchBarNext/index.tsx @@ -0,0 +1,17 @@ +/* + * 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 { SearchBarNext } from './SearchBarNext'; diff --git a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx new file mode 100644 index 0000000000..5b2133e573 --- /dev/null +++ b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx @@ -0,0 +1,67 @@ +/* + * 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 } from '@backstage/core'; +import { Divider, List, ListItem, ListItemText } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; + +import { useSearch } from '../SearchContext'; + +const DefaultResultListItem = ({ result }: any) => { + return ( + + + + + + + ); +}; + +export const SearchResultNext = () => { + const { + resultState: { loading, error, value }, + } = useSearch(); + + if (loading) { + return ; + } + if (error) { + return ( + + Error encountered while fetching search results. {error.toString()} + + ); + } + + if (!value) { + return ; + } + + return ( + + {value.results.map(result => ( + <> + + + ))} + + ); +}; diff --git a/plugins/search/src/components/SearchResultNext/index.tsx b/plugins/search/src/components/SearchResultNext/index.tsx new file mode 100644 index 0000000000..1a21491289 --- /dev/null +++ b/plugins/search/src/components/SearchResultNext/index.tsx @@ -0,0 +1,17 @@ +/* + * 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 { SearchResultNext } from './SearchResultNext'; From f0ffd95326393de2bd450dfdbbf95320c8003cfb Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Tue, 18 May 2021 19:41:49 +0200 Subject: [PATCH 03/56] refactor search page using new components Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../SearchPageNext/SearchPageNext.tsx | 73 ++++++------------- 1 file changed, 24 insertions(+), 49 deletions(-) diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index a6655e37ac..ce9e203cf1 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -13,59 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Content, - Header, - Lifecycle, - Page, - useQueryParamState, -} from '@backstage/core'; + +import React from 'react'; +import { Content, Header, Lifecycle, Page } from '@backstage/core'; import { Grid } from '@material-ui/core'; -import React, { useEffect, useState } from 'react'; -import { useDebounce } from 'react-use'; -import { SearchBar } from '../SearchBar'; -import { SearchResult } from '../SearchResult'; +import { SearchBarNext } from '../SearchBarNext'; +import { SearchResultNext } from '../SearchResultNext'; +import { SearchContextProvider } from '../SearchContext'; export const SearchPageNext = () => { - const [queryString, setQueryString] = useQueryParamState('query'); - const [searchQuery, setSearchQuery] = useState(queryString ?? ''); - - const handleSearch = (event: React.ChangeEvent) => { - event.preventDefault(); - setSearchQuery(event.target.value); - }; - - useEffect(() => setSearchQuery(queryString ?? ''), [queryString]); - - useDebounce( - () => { - setQueryString(searchQuery); - }, - 200, - [searchQuery], - ); - - const handleClearSearchBar = () => { - setSearchQuery(''); - }; - return ( - -
} /> - - - - + + +
} /> + + + + + + + {/* filter component should be rendered here */} +

filter

+
+ + +
- - - - -
- + + + ); }; From 83ef71d3f70b87e77f726c74073fd27180a1da5f Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Tue, 18 May 2021 19:42:05 +0200 Subject: [PATCH 04/56] export components Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- plugins/search/src/components/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index f571c61373..ae0bc2cdca 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -16,7 +16,10 @@ export * from './Filters'; export * from './SearchBar'; +export * from './SearchBarNext'; export * from './SearchPage'; export * from './SearchPageNext'; export * from './SearchResult'; +export * from './SearchResultNext'; export * from './SidebarSearch'; +export * from './SearchContext'; From b26eaef405f9d4297935b6d6c20588c3b23e4916 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 19 May 2021 12:12:11 +0200 Subject: [PATCH 05/56] new filters component Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../FiltersNext/FiltersButtonNext.tsx | 56 +++++++ .../components/FiltersNext/FiltersNext.tsx | 139 ++++++++++++++++++ .../src/components/FiltersNext/index.tsx | 18 +++ .../SearchPageNext/SearchPageNext.tsx | 12 +- plugins/search/src/components/index.tsx | 1 + 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 plugins/search/src/components/FiltersNext/FiltersButtonNext.tsx create mode 100644 plugins/search/src/components/FiltersNext/FiltersNext.tsx create mode 100644 plugins/search/src/components/FiltersNext/index.tsx diff --git a/plugins/search/src/components/FiltersNext/FiltersButtonNext.tsx b/plugins/search/src/components/FiltersNext/FiltersButtonNext.tsx new file mode 100644 index 0000000000..8da6e26028 --- /dev/null +++ b/plugins/search/src/components/FiltersNext/FiltersButtonNext.tsx @@ -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 FiltersButtonNext = ({ + numberOfSelectedFilters, + handleToggleFilters, +}: FiltersButtonProps) => { + const classes = useStyles(); + + return ( +
+ + + + + Filters ({numberOfSelectedFilters ? numberOfSelectedFilters : 0}) + +
+ ); +}; diff --git a/plugins/search/src/components/FiltersNext/FiltersNext.tsx b/plugins/search/src/components/FiltersNext/FiltersNext.tsx new file mode 100644 index 0000000000..770fe8078d --- /dev/null +++ b/plugins/search/src/components/FiltersNext/FiltersNext.tsx @@ -0,0 +1,139 @@ +/* + * 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, + Card, + 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 FilterOptions = { + kind: Array; + lifecycle: Array; +}; + +type FiltersProps = { + definitions: FilterDefinition[]; +}; + +type ValuedFilterProps = { + fieldName: string; + values: string[]; +}; + +export enum FilterType { + CHECKBOX = 'checkbox', + SELECT = 'select', +} + +export type NewFilterDefinition = { + component: any; + props: any; +}; + +export type FilterDefinition = { + field: string; + type: FilterType; + values: string[]; +}; + +const CheckBoxFilter = ({ fieldName, values }: ValuedFilterProps) => { + return ( + + {fieldName} + + {values.map((value: string) => ( + {}}> + + + + ))} + + + ); +}; + +const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { + return ( + + {fieldName} + + + ); +}; + +export const FiltersNext = ({ definitions }: FiltersProps) => { + const classes = useStyles(); + + return ( + + {definitions.map(definition => { + switch (definition.type) { + case 'checkbox': + return ( + + ); + case 'select': + return ( + + ); + default: + return null; + } + })} + + ); +}; diff --git a/plugins/search/src/components/FiltersNext/index.tsx b/plugins/search/src/components/FiltersNext/index.tsx new file mode 100644 index 0000000000..574a31e93f --- /dev/null +++ b/plugins/search/src/components/FiltersNext/index.tsx @@ -0,0 +1,18 @@ +/* + * 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 { FiltersButtonNext } from './FiltersButtonNext'; +export { FiltersNext, FilterType } from './FiltersNext'; diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index ce9e203cf1..ca942aabb1 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -20,6 +20,15 @@ import { Grid } from '@material-ui/core'; import { SearchBarNext } from '../SearchBarNext'; import { SearchResultNext } from '../SearchResultNext'; import { SearchContextProvider } from '../SearchContext'; +import { FiltersNext, FilterType } from '../FiltersNext'; + +const exampleFilterDefinition = [ + { + field: 'lifecycle', + type: FilterType.CHECKBOX, + values: ['exerpimental', 'production'], + }, +]; export const SearchPageNext = () => { return ( @@ -32,8 +41,7 @@ export const SearchPageNext = () => { - {/* filter component should be rendered here */} -

filter

+
diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index ae0bc2cdca..27491d6c93 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -15,6 +15,7 @@ */ export * from './Filters'; +export * from './FiltersNext'; export * from './SearchBar'; export * from './SearchBarNext'; export * from './SearchPage'; From fa753e4edd6a147caf87d3701c91e72ddfa24e67 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 19 May 2021 21:48:25 +0200 Subject: [PATCH 06/56] split out state in search context Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../SearchContext/SearchContext.tsx | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index ca1a82cde0..b0bcff1f25 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -22,14 +22,21 @@ import React, { } from 'react'; import { useAsync } from 'react-use'; import { useApi } from '@backstage/core'; -import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/search-common'; import { searchApiRef } from '../../apis'; import { AsyncState } from 'react-use/lib/useAsync'; +import { JsonObject } from '@backstage/config'; type SearchContextValue = { - resultState: AsyncState; - queryState: SearchQuery; - setQueryState: React.Dispatch>; + result: AsyncState; + term: string; + setTerm: React.Dispatch>; + types: string[]; + setTypes: React.Dispatch>; + filters: JsonObject; + setFilters: React.Dispatch>; + pageCursor: string; + setPageCursor: React.Dispatch>; }; const SearchContext = createContext({} as SearchContextValue); @@ -38,23 +45,39 @@ export const SearchContextProvider = ({ initialState = { term: '', pageCursor: '', + filters: {}, types: ['*'], }, children, }: PropsWithChildren<{ initialState?: any }>) => { const searchApi = useApi(searchApiRef); - const [queryState, setQueryState] = useState(initialState); + const [pageCursor, setPageCursor] = useState(initialState.pageCursor); + const [filters, setFilters] = useState(initialState.filters); + const [term, setTerm] = useState(initialState.term); + const [types, setTypes] = useState(initialState.types); - const resultState = useAsync( + const result = useAsync( () => searchApi._alphaPerformSearch({ - term: queryState.term, - pageCursor: queryState.pageCursor, + term, + filters, + pageCursor, + types, }), - [queryState.term], + [term, filters, types, pageCursor], ); - const value: SearchContextValue = { resultState, queryState, setQueryState }; + const value: SearchContextValue = { + result, + filters, + setFilters, + term, + setTerm, + types, + setTypes, + pageCursor, + setPageCursor, + }; return ; }; From 982849c5b7b9aa926afac05d1ffd421ffdc31cc0 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 19 May 2021 21:49:23 +0200 Subject: [PATCH 07/56] wip new filter component Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../components/FiltersNext/FiltersNext.tsx | 108 ++++++++++++++---- 1 file changed, 85 insertions(+), 23 deletions(-) diff --git a/plugins/search/src/components/FiltersNext/FiltersNext.tsx b/plugins/search/src/components/FiltersNext/FiltersNext.tsx index 770fe8078d..45de27b032 100644 --- a/plugins/search/src/components/FiltersNext/FiltersNext.tsx +++ b/plugins/search/src/components/FiltersNext/FiltersNext.tsx @@ -14,33 +14,41 @@ * limitations under the License. */ -import React from 'react'; import { - makeStyles, - Typography, Card, CardContent, - Select, Checkbox, + FormControl, + InputLabel, List, + MenuItem, ListItem, ListItemText, - MenuItem, + makeStyles, + Select, } from '@material-ui/core'; +import React from 'react'; +import { useSearch } from '../SearchContext'; -const useStyles = makeStyles(theme => ({ +const useFilterStyles = makeStyles({ filters: { background: 'transparent', boxShadow: '0px 0px 0px 0px', }, - // checkbox: { - // padding: theme.spacing(0, 1, 0, 1), - // }, - // dropdown: { - // width: '100%', - // }, +}); + +const useCheckBoxStyles = makeStyles(theme => ({ + checkbox: { + padding: theme.spacing(0, 1, 0, 1), + }, })); +const useSelectStyles = makeStyles({ + select: { + width: '100%', + }, +}); + export type FilterOptions = { kind: Array; lifecycle: Array; @@ -72,13 +80,45 @@ export type FilterDefinition = { }; const CheckBoxFilter = ({ fieldName, values }: ValuedFilterProps) => { + const { filters, setFilters } = useSearch(); + const classes = useCheckBoxStyles(); + + const setCheckboxFilter = (filter: string) => { + const newFilters = filters; + const currentValues = newFilters[fieldName] as string[]; + + if (!filter) return; + + if (!currentValues) { + setFilters({ ...filters, [fieldName]: [filter] }); + } else if (!currentValues?.includes(filter)) { + setFilters({ + ...filters, + [fieldName]: [...currentValues, filter], + }); + } else { + const filterToDelete = currentValues.find(value => value === filter); + if (filterToDelete) { + currentValues.splice(currentValues.indexOf(filterToDelete), 1); + + setFilters({ ...filters, [fieldName]: currentValues }); + } + } + }; return ( - {fieldName} - + {fieldName} + {values.map((value: string) => ( - {}}> + setCheckboxFilter(value)} + > { }; const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { + const { filters, setFilters } = useSearch(); + const classes = useSelectStyles(); + + const setSelectFilter = (filter: string) => { + const newFilters = filters; + if (newFilters[fieldName] && filter === '') { + delete newFilters[fieldName]; + setFilters({ newFilters }); + } else { + setFilters({ ...filters, [fieldName]: filter as string }); + } + }; + return ( - {fieldName} - ) => + setSelectFilter(e?.target?.value) + } + > + + All - ))} - + {values.map((value: string) => ( + {value} + ))} + + ); }; export const FiltersNext = ({ definitions }: FiltersProps) => { - const classes = useStyles(); + const classes = useFilterStyles(); return ( From af6a718e825cda8b6ed706d69702499089bb4706 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 19 May 2021 21:50:59 +0200 Subject: [PATCH 08/56] change term presence in lunr query Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- plugins/search-backend-node/src/engines/LunrSearchEngine.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index cc8c5bb982..bbe7eb7905 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -44,6 +44,7 @@ export class LunrSearchEngine implements SearchEngine { types, }: SearchQuery): ConcreteLunrQuery => { let lunrQueryFilters; + const lunrTerm = term ? `+${term}` : ''; if (filters) { lunrQueryFilters = Object.entries(filters) .map(([key, value]) => ` +${key}:${value}`) @@ -51,7 +52,7 @@ export class LunrSearchEngine implements SearchEngine { } return { - lunrQueryString: `${term}${lunrQueryFilters || ''}`, + lunrQueryString: `${lunrTerm}${lunrQueryFilters || ''}`, documentTypes: types || ['*'], }; }; From d57dc2dce9e4b74236d0699e11eea2454c437d79 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 19 May 2021 21:51:25 +0200 Subject: [PATCH 09/56] fixups Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../src/components/FiltersNext/FiltersNext.tsx | 2 ++ .../components/SearchBarNext/SearchBarNext.tsx | 17 ++++++----------- .../SearchPageNext/SearchPageNext.tsx | 11 ++++++++--- .../SearchResultNext/SearchResultNext.tsx | 9 +++++---- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/plugins/search/src/components/FiltersNext/FiltersNext.tsx b/plugins/search/src/components/FiltersNext/FiltersNext.tsx index 45de27b032..6b833dbf5a 100644 --- a/plugins/search/src/components/FiltersNext/FiltersNext.tsx +++ b/plugins/search/src/components/FiltersNext/FiltersNext.tsx @@ -181,6 +181,7 @@ export const FiltersNext = ({ definitions }: FiltersProps) => { case 'checkbox': return ( @@ -188,6 +189,7 @@ export const FiltersNext = ({ definitions }: FiltersProps) => { case 'select': return ( diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index 1e4a59ac8b..bec296c42a 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -34,16 +34,13 @@ const useStyles = makeStyles(() => ({ export const SearchBarNext = () => { const classes = useStyles(); - const { - queryState: { term }, - setQueryState, - } = useSearch(); + const { term, setTerm, setPageCursor } = useSearch(); const [queryString, setQueryString] = useQueryParamState('query'); useEffect(() => { - setQueryState({ term: queryString ?? '', pageCursor: '' }); - }, [queryString, setQueryState]); + setTerm(queryString ?? ''); + }, [queryString, setTerm]); useDebounce( () => { @@ -55,14 +52,12 @@ export const SearchBarNext = () => { const handleSearch = (event: React.ChangeEvent | React.FormEvent) => { event.preventDefault(); - setQueryState({ - term: (event.target as HTMLInputElement).value, - pageCursor: '', - }); + setTerm((event.target as HTMLInputElement).value as string); }; const handleClearSearchBar = () => { - setQueryState({ term: '', pageCursor: '' }); + setTerm(''); + setPageCursor(''); }; return ( diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index ca942aabb1..441ca2b6a2 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -22,11 +22,16 @@ import { SearchResultNext } from '../SearchResultNext'; import { SearchContextProvider } from '../SearchContext'; import { FiltersNext, FilterType } from '../FiltersNext'; -const exampleFilterDefinition = [ +const defaultFilterDefinitions = [ + { + field: 'kind', + type: FilterType.SELECT, + values: ['Component', 'Template'], + }, { field: 'lifecycle', type: FilterType.CHECKBOX, - values: ['exerpimental', 'production'], + values: ['experimental', 'production'], }, ]; @@ -41,7 +46,7 @@ export const SearchPageNext = () => { - + diff --git a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx index 5b2133e573..af772c4967 100644 --- a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx +++ b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx @@ -37,7 +37,7 @@ const DefaultResultListItem = ({ result }: any) => { export const SearchResultNext = () => { const { - resultState: { loading, error, value }, + result: { loading, error, value }, } = useSearch(); if (loading) { @@ -58,9 +58,10 @@ export const SearchResultNext = () => { return ( {value.results.map(result => ( - <> - - + ))} ); From 5bd3611406d96af0c6df2ac747c14483a1284403 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Jun 2021 16:25:24 +0200 Subject: [PATCH 10/56] add backtage config package to deps Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- plugins/search/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/search/package.json b/plugins/search/package.json index 5483eda4c2..cc42bb05ed 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -33,6 +33,7 @@ "@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", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", From 94d1d852e1b23a32fda665ac23ed15582c81ff64 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 21 May 2021 09:41:55 +0200 Subject: [PATCH 11/56] update lunr search query filters Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../src/engines/LunrSearchEngine.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index bbe7eb7905..77bba3ff6c 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -47,7 +47,27 @@ export class LunrSearchEngine implements SearchEngine { const lunrTerm = term ? `+${term}` : ''; if (filters) { lunrQueryFilters = Object.entries(filters) - .map(([key, value]) => ` +${key}:${value}`) + .map(([field, value]) => { + // Require that the given field has the given value (with +). + if (['string', 'number', 'boolean'].includes(typeof value)) { + return ` +${field}:${value}`; + } + + // Illustrate how multi-value filters could work. + if (Array.isArray(value)) { + // But warn that Lurn supports this poorly. + this.logger.warn( + `Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`, + ); + return ` ${value.map(v => { + return `${field}:${v}`; + })}`; + } + + // Log a warning or something about unknown filter value + this.logger.warn(`Unknown filter type used on field ${field}`); + return ''; + }) .join(''); } From b614549b3ed054aa225bf8a6ede6506665986cd9 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 21 May 2021 09:44:08 +0200 Subject: [PATCH 12/56] export component as extensions Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../FiltersButtonNext.tsx | 0 .../SearchFiltersNext.tsx} | 2 +- .../index.tsx | 2 +- .../SearchPageNext/SearchPageNext.tsx | 4 +-- plugins/search/src/components/index.tsx | 2 +- plugins/search/src/index.ts | 3 ++ plugins/search/src/plugin.ts | 28 +++++++++++++++++++ 7 files changed, 36 insertions(+), 5 deletions(-) rename plugins/search/src/components/{FiltersNext => SearchFiltersNext}/FiltersButtonNext.tsx (100%) rename plugins/search/src/components/{FiltersNext/FiltersNext.tsx => SearchFiltersNext/SearchFiltersNext.tsx} (98%) rename plugins/search/src/components/{FiltersNext => SearchFiltersNext}/index.tsx (90%) diff --git a/plugins/search/src/components/FiltersNext/FiltersButtonNext.tsx b/plugins/search/src/components/SearchFiltersNext/FiltersButtonNext.tsx similarity index 100% rename from plugins/search/src/components/FiltersNext/FiltersButtonNext.tsx rename to plugins/search/src/components/SearchFiltersNext/FiltersButtonNext.tsx diff --git a/plugins/search/src/components/FiltersNext/FiltersNext.tsx b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx similarity index 98% rename from plugins/search/src/components/FiltersNext/FiltersNext.tsx rename to plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx index 6b833dbf5a..92c65f4a57 100644 --- a/plugins/search/src/components/FiltersNext/FiltersNext.tsx +++ b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx @@ -171,7 +171,7 @@ const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { ); }; -export const FiltersNext = ({ definitions }: FiltersProps) => { +export const SearchFiltersNext = ({ definitions }: FiltersProps) => { const classes = useFilterStyles(); return ( diff --git a/plugins/search/src/components/FiltersNext/index.tsx b/plugins/search/src/components/SearchFiltersNext/index.tsx similarity index 90% rename from plugins/search/src/components/FiltersNext/index.tsx rename to plugins/search/src/components/SearchFiltersNext/index.tsx index 574a31e93f..d45eadc79a 100644 --- a/plugins/search/src/components/FiltersNext/index.tsx +++ b/plugins/search/src/components/SearchFiltersNext/index.tsx @@ -15,4 +15,4 @@ */ export { FiltersButtonNext } from './FiltersButtonNext'; -export { FiltersNext, FilterType } from './FiltersNext'; +export { SearchFiltersNext, FilterType } from './SearchFiltersNext'; diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index 441ca2b6a2..691f01b2cb 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -20,7 +20,7 @@ import { Grid } from '@material-ui/core'; import { SearchBarNext } from '../SearchBarNext'; import { SearchResultNext } from '../SearchResultNext'; import { SearchContextProvider } from '../SearchContext'; -import { FiltersNext, FilterType } from '../FiltersNext'; +import { SearchFiltersNext, FilterType } from '../SearchFiltersNext'; const defaultFilterDefinitions = [ { @@ -46,7 +46,7 @@ export const SearchPageNext = () => { - + diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 27491d6c93..aa9a443f21 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -15,7 +15,7 @@ */ export * from './Filters'; -export * from './FiltersNext'; +export * from './SearchFiltersNext'; export * from './SearchBar'; export * from './SearchBarNext'; export * from './SearchPage'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 519d7fcf66..367b195f9d 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -20,6 +20,9 @@ export { searchPlugin as plugin, SearchPage, SearchPageNext, + SearchBarNext, + SearchResultNext, + SearchFiltersNext, } from './plugin'; export { Filters, diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index e3e953e07d..03322b33aa 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -19,6 +19,7 @@ import { createRouteRef, createRoutableExtension, discoveryApiRef, + createComponentExtension, } from '@backstage/core'; import { SearchClient, searchApiRef } from './apis'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; @@ -70,3 +71,30 @@ export const SearchPageNext = searchPlugin.provide( mountPoint: rootNextRouteRef, }), ); + +export const SearchBarNext = searchPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/SearchBarNext').then(m => m.SearchBarNext), + }, + }), +); + +export const SearchResultNext = searchPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/SearchResultNext').then(m => m.SearchResultNext), + }, + }), +); + +export const SearchFiltersNext = searchPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/SearchFiltersNext').then(m => m.SearchFiltersNext), + }, + }), +); From 8829e8228e9e8a0d693e349aad125d142f8cc657 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 21 May 2021 09:45:54 +0200 Subject: [PATCH 13/56] wip composable result list Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- packages/search-common/src/types.ts | 1 + .../src/engines/LunrSearchEngine.ts | 2 +- .../SearchResultNext/SearchResultNext.tsx | 47 ++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index daa5823424..c26ecdcb4c 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -23,6 +23,7 @@ export interface SearchQuery { } export interface SearchResult { + type: string; document: IndexableDocument; } diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 77bba3ff6c..3829693d4e 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -146,7 +146,7 @@ export class LunrSearchEngine implements SearchEngine { // Translate results into SearchResultSet const resultSet: SearchResultSet = { results: results.map(d => { - return { document: this.docStore[d.ref] }; + return { type: 'techdocs', document: this.docStore[d.ref] }; }), }; diff --git a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx index af772c4967..53fc4ff17b 100644 --- a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx +++ b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx @@ -35,6 +35,21 @@ const DefaultResultListItem = ({ result }: any) => { ); }; +const TechDocsResultListItem = ({ result }: any) => { + return ( + + + + + + + ); +}; + export const SearchResultNext = () => { const { result: { loading, error, value }, @@ -57,12 +72,32 @@ export const SearchResultNext = () => { return ( - {value.results.map(result => ( - - ))} + {value.results.map(result => { + // Render different result items based on document type + switch (result.type) { + case 'software-catalog': + return ( + + ); + case 'techdocs': + return ( + + ); + default: + return ( + + ); + } + })} ); }; From 1f3e6dff881be05a3b66f8d0329a3c138550abe7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 21 May 2021 14:17:35 +0200 Subject: [PATCH 14/56] Pass result type along from search engine. Signed-off-by: Eric Peterson --- packages/search-common/src/types.ts | 2 +- .../src/IndexBuilder.test.ts | 2 +- .../search-backend-node/src/IndexBuilder.ts | 2 +- .../src/engines/LunrSearchEngine.ts | 43 ++++++++++++++----- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index c26ecdcb4c..07a4296b85 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -66,5 +66,5 @@ export interface DocumentCollator { * additional metadata. */ export interface DocumentDecorator { - execute(documents: IndexableDocument[]): Promise; + execute(type: string, documents: IndexableDocument[]): Promise; } diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index e1dc511b97..c7a2a7817a 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -30,7 +30,7 @@ class TestDocumentCollator implements DocumentCollator { } class TestDocumentDecorator implements DocumentDecorator { - async execute(documents: IndexableDocument[]) { + async execute(_type: string, documents: IndexableDocument[]) { return documents; } } diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 71d374e704..acaa18fc03 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -113,7 +113,7 @@ export class IndexBuilder { this.logger.debug( `Decorating ${type} documents via ${decorators[i].constructor.name}`, ); - documents = await decorators[i].execute(documents); + documents = await decorators[i].execute(type, documents); } if (!documents || documents.length === 0) { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 3829693d4e..8bff26a784 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -28,6 +28,11 @@ type ConcreteLunrQuery = { documentTypes: string[]; }; +type LunrResultEnvelope = { + result: lunr.Index.Result; + type: string; +}; + export class LunrSearchEngine implements SearchEngine { protected lunrIndices: Record = {}; protected docStore: Record; @@ -104,13 +109,22 @@ export class LunrSearchEngine implements SearchEngine { query, ) as ConcreteLunrQuery; - const results: lunr.Index.Result[] = []; + const results: LunrResultEnvelope[] = []; if (documentTypes.length === 1 && documentTypes[0] === '*') { // Iterate over all this.lunrIndex values. - Object.values(this.lunrIndices).forEach(i => { + Object.keys(this.lunrIndices).forEach(type => { try { - results.push(...i.search(lunrQueryString)); + results.push( + ...this.lunrIndices[type] + .search(lunrQueryString) + .map(result => { + return { + result: result, + type: type, + }; + }) + ); } catch (err) { // if a field does not exist on a index, we can see that as a no-match if ( @@ -123,10 +137,19 @@ export class LunrSearchEngine implements SearchEngine { } else { // Iterate over the filtered list of this.lunrIndex keys. Object.keys(this.lunrIndices) - .filter(d => documentTypes.includes(d)) - .forEach(d => { + .filter(type => documentTypes.includes(type)) + .forEach(type => { try { - results.push(...this.lunrIndices[d].search(lunrQueryString)); + results.push( + ...this.lunrIndices[type] + .search(lunrQueryString) + .map(result => { + return { + result: result, + type: type, + }; + }) + ); } catch (err) { // if a field does not exist on a index, we can see that as a no-match if ( @@ -140,16 +163,16 @@ export class LunrSearchEngine implements SearchEngine { // Sort results. results.sort((doc1, doc2) => { - return doc2.score - doc1.score; + return doc2.result.score - doc1.result.score; }); // Translate results into SearchResultSet - const resultSet: SearchResultSet = { + const realResultSet: SearchResultSet = { results: results.map(d => { - return { type: 'techdocs', document: this.docStore[d.ref] }; + return { type: d.type, document: this.docStore[d.result.ref] }; }), }; - return Promise.resolve(resultSet); + return Promise.resolve(realResultSet); } } From 94e13c8892dcd1016f4c476c5c501e58db4d4d8e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 21 May 2021 14:18:33 +0200 Subject: [PATCH 15/56] Making Search page composable. Signed-off-by: Eric Peterson --- packages/app/src/App.tsx | 7 +- .../app/src/components/search/SearchPage.tsx | 78 +++++++++++++++++++ .../CatalogResultListItem.tsx | 51 ++++++++++++ .../components/CatalogResultListItem/index.ts | 17 ++++ plugins/catalog/src/index.ts | 1 + plugins/search/package.json | 1 + .../DefaultResultListItem.tsx | 35 +++++++++ .../components/DefaultResultListItem/index.ts | 18 +++++ .../SearchPageNext/SearchPageNext.tsx | 36 +-------- .../SearchResultNext/SearchResultNext.tsx | 71 ++--------------- plugins/search/src/components/index.tsx | 1 + plugins/search/src/index.ts | 4 + plugins/search/src/plugin.ts | 9 +++ 13 files changed, 230 insertions(+), 99 deletions(-) create mode 100644 packages/app/src/components/search/SearchPage.tsx create mode 100644 plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx create mode 100644 plugins/catalog/src/components/CatalogResultListItem/index.ts create mode 100644 plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx create mode 100644 plugins/search/src/components/DefaultResultListItem/index.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c5e5033973..04f618c583 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -54,6 +54,7 @@ import { Navigate, Route } from 'react-router'; import { apis } from './apis'; import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; +import { searchPage } from './components/search/SearchPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; @@ -121,8 +122,10 @@ const routes = ( } /> } - /> + element={} + > + {searchPage} + } /> +
} /> + + + + + + + + + + + {({results}) => ( + + {results.map(result => { + switch (result.type) { + case 'software-catalog': + return ( + + ); + default: + return ( + + ); + } + })} + + )} + + + + + +); diff --git a/plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx b/plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx new file mode 100644 index 0000000000..88a1f9f1d6 --- /dev/null +++ b/plugins/catalog/src/components/CatalogResultListItem/CatalogResultListItem.tsx @@ -0,0 +1,51 @@ +/* + * 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 + * l +imitations under the License. + */ + +import React from 'react'; +import { Link } from '@backstage/core'; +import { Box, Chip, Divider, ListItem, ListItemText, makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + marginBottom: '1rem', + }, +}); + +export const CatalogResultListItem = ({ result }: any) => { + const classes = useStyles(); + return ( + + + + + {result.kind && ()} + {result.lifecycle && ()} + + + + + ); +}; diff --git a/plugins/catalog/src/components/CatalogResultListItem/index.ts b/plugins/catalog/src/components/CatalogResultListItem/index.ts new file mode 100644 index 0000000000..8f418c1dc7 --- /dev/null +++ b/plugins/catalog/src/components/CatalogResultListItem/index.ts @@ -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 { CatalogResultListItem } from './CatalogResultListItem'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index f7506b534a..052f055dc0 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -15,6 +15,7 @@ */ export { AboutCard } from './components/AboutCard'; +export { CatalogResultListItem } from './components/CatalogResultListItem'; export { EntityLayout } from './components/EntityLayout'; export { EntityPageLayout } from './components/EntityPageLayout'; export { CatalogTable } from './components/CatalogTable'; diff --git a/plugins/search/package.json b/plugins/search/package.json index cc42bb05ed..c767c923db 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -41,6 +41,7 @@ "qs": "^6.9.4", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx new file mode 100644 index 0000000000..09ef325f85 --- /dev/null +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -0,0 +1,35 @@ +/* + * 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 + * l +imitations under the License. + */ + +import React from 'react'; +import { Link } from '@backstage/core'; +import { ListItem, ListItemText, Divider } from '@material-ui/core'; + +export const DefaultResultListItem = ({ result }: any) => { + return ( + + + + + + + ); +}; diff --git a/plugins/search/src/components/DefaultResultListItem/index.ts b/plugins/search/src/components/DefaultResultListItem/index.ts new file mode 100644 index 0000000000..f195f62929 --- /dev/null +++ b/plugins/search/src/components/DefaultResultListItem/index.ts @@ -0,0 +1,18 @@ +/* + * 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 + * l +imitations under the License. + */ + +export { DefaultResultListItem } from './DefaultResultListItem'; diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index 691f01b2cb..e86c887f59 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -15,45 +15,13 @@ */ import React from 'react'; -import { Content, Header, Lifecycle, Page } from '@backstage/core'; -import { Grid } from '@material-ui/core'; -import { SearchBarNext } from '../SearchBarNext'; -import { SearchResultNext } from '../SearchResultNext'; +import { Outlet } from 'react-router'; import { SearchContextProvider } from '../SearchContext'; -import { SearchFiltersNext, FilterType } from '../SearchFiltersNext'; - -const defaultFilterDefinitions = [ - { - field: 'kind', - type: FilterType.SELECT, - values: ['Component', 'Template'], - }, - { - field: 'lifecycle', - type: FilterType.CHECKBOX, - values: ['experimental', 'production'], - }, -]; export const SearchPageNext = () => { return ( - -
} /> - - - - - - - - - - - - - - + ); }; diff --git a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx index 53fc4ff17b..ec738e839c 100644 --- a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx +++ b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx @@ -13,44 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EmptyState, Link, Progress } from '@backstage/core'; -import { Divider, List, ListItem, ListItemText } from '@material-ui/core'; +import { EmptyState, Progress } from '@backstage/core'; +import { SearchResult } from '@backstage/search-common'; import { Alert } from '@material-ui/lab'; import React from 'react'; import { useSearch } from '../SearchContext'; -const DefaultResultListItem = ({ result }: any) => { - return ( - - - - - - - ); -}; +type ChildrenArguments = { + results: SearchResult[]; +} -const TechDocsResultListItem = ({ result }: any) => { - return ( - - - - - - - ); -}; - -export const SearchResultNext = () => { +export const SearchResultNext = ({ children }: { children: (results: ChildrenArguments) => JSX.Element}) => { const { result: { loading, error, value }, } = useSearch(); @@ -67,37 +41,8 @@ export const SearchResultNext = () => { } if (!value) { - return ; + return ; } - return ( - - {value.results.map(result => { - // Render different result items based on document type - switch (result.type) { - case 'software-catalog': - return ( - - ); - case 'techdocs': - return ( - - ); - default: - return ( - - ); - } - })} - - ); + return children({ results: value.results }); }; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index aa9a443f21..597bfabfb1 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -22,5 +22,6 @@ export * from './SearchPage'; export * from './SearchPageNext'; export * from './SearchResult'; export * from './SearchResultNext'; +export * from './DefaultResultListItem'; export * from './SidebarSearch'; export * from './SearchContext'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 367b195f9d..e5894aced4 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -23,11 +23,15 @@ export { SearchBarNext, SearchResultNext, SearchFiltersNext, + DefaultResultListItem, } from './plugin'; export { Filters, FiltersButton, + FilterType, SearchBar, + SearchContextProvider, + useSearch, SearchPage as Router, SearchResult, SidebarSearch, diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 03322b33aa..3de59fe1d0 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -90,6 +90,15 @@ export const SearchResultNext = searchPlugin.provide( }), ); +export const DefaultResultListItem= searchPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/DefaultResultListItem').then(m => m.DefaultResultListItem), + }, + }), +); + export const SearchFiltersNext = searchPlugin.provide( createComponentExtension({ component: { From 578028c9a6cbe21e147a6c3252e0b8db0610d474 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 24 May 2021 09:07:36 +0200 Subject: [PATCH 16/56] consistent composability between result list items and filters Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../app/src/components/search/SearchPage.tsx | 41 ++++++++++++++++--- .../SearchFiltersNext/SearchFiltersNext.tsx | 35 +++------------- .../components/SearchFiltersNext/index.tsx | 7 +++- plugins/search/src/index.ts | 2 + 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 3aff554b42..59b4168a18 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -17,9 +17,15 @@ import React from 'react'; import { Content, Header, Lifecycle, Page } from '@backstage/core'; import { Grid, List } from '@material-ui/core'; -import { SearchBarNext } from '@backstage/plugin-search'; -import { SearchResultNext, DefaultResultListItem } from '@backstage/plugin-search'; -import { SearchFiltersNext, FilterType } from '@backstage/plugin-search'; +import { + SearchBarNext, + SearchResultNext, + DefaultResultListItem, + SearchFiltersNext, + FilterType, + CheckBoxFilter, + SelectFilter, +} from '@backstage/plugin-search'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; const filterDefinitions = [ @@ -44,11 +50,36 @@ export const searchPage = ( - + + <> + {filterDefinitions.map(definition => { + switch (definition.type) { + case 'checkbox': + return ( + + ); + case 'select': + return ( + + ); + default: + return null; + } + })} + + - {({results}) => ( + {({ results }) => ( {results.map(result => { switch (result.type) { diff --git a/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx index 92c65f4a57..b6536276c3 100644 --- a/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx +++ b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx @@ -55,7 +55,7 @@ export type FilterOptions = { }; type FiltersProps = { - definitions: FilterDefinition[]; + children: React.ReactChild; }; type ValuedFilterProps = { @@ -79,7 +79,7 @@ export type FilterDefinition = { values: string[]; }; -const CheckBoxFilter = ({ fieldName, values }: ValuedFilterProps) => { +export const CheckBoxFilter = ({ fieldName, values }: ValuedFilterProps) => { const { filters, setFilters } = useSearch(); const classes = useCheckBoxStyles(); @@ -134,7 +134,7 @@ const CheckBoxFilter = ({ fieldName, values }: ValuedFilterProps) => { ); }; -const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { +export const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { const { filters, setFilters } = useSearch(); const classes = useSelectStyles(); @@ -171,33 +171,8 @@ const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { ); }; -export const SearchFiltersNext = ({ definitions }: FiltersProps) => { +export const SearchFiltersNext = ({ children }: FiltersProps) => { const classes = useFilterStyles(); - return ( - - {definitions.map(definition => { - switch (definition.type) { - case 'checkbox': - return ( - - ); - case 'select': - return ( - - ); - default: - return null; - } - })} - - ); + return {children}; }; diff --git a/plugins/search/src/components/SearchFiltersNext/index.tsx b/plugins/search/src/components/SearchFiltersNext/index.tsx index d45eadc79a..999db8deb5 100644 --- a/plugins/search/src/components/SearchFiltersNext/index.tsx +++ b/plugins/search/src/components/SearchFiltersNext/index.tsx @@ -15,4 +15,9 @@ */ export { FiltersButtonNext } from './FiltersButtonNext'; -export { SearchFiltersNext, FilterType } from './SearchFiltersNext'; +export { + SearchFiltersNext, + CheckBoxFilter, + SelectFilter, + FilterType, +} from './SearchFiltersNext'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index e5894aced4..4d8d9c08a2 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -29,6 +29,8 @@ export { Filters, FiltersButton, FilterType, + CheckBoxFilter, + SelectFilter, SearchBar, SearchContextProvider, useSearch, From cdfa7d69df13221d9501335b97024b0d5a43b3d0 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 24 May 2021 09:21:29 +0200 Subject: [PATCH 17/56] fix margin Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../src/components/SearchFiltersNext/SearchFiltersNext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx index b6536276c3..91c193f54e 100644 --- a/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx +++ b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx @@ -151,7 +151,7 @@ export const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { return ( - {fieldName} + {fieldName} ) => + setSelectFilter(e?.target?.value) + } + > + + All + + {values && + values.map((value: string) => ( + {value} + ))} + + + ); +}; + +const SearchFilterNext = ({ component: Element, ...props }: Props) => { + return ; +}; + +SearchFilterNext.Checkbox = (props: Omit) => ( + +); +SearchFilterNext.Select = (props: Omit) => ( + +); + +export { SearchFilterNext }; diff --git a/plugins/search/src/components/SearchFilterNext/index.ts b/plugins/search/src/components/SearchFilterNext/index.ts new file mode 100644 index 0000000000..322abe64d7 --- /dev/null +++ b/plugins/search/src/components/SearchFilterNext/index.ts @@ -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 { SearchFilterNext } from './SearchFilterNext'; diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index e86c887f59..f3a4efc32c 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -15,12 +15,26 @@ */ import React from 'react'; -import { Outlet } from 'react-router'; +import qs from 'qs'; +import { Outlet, useLocation } from 'react-router'; +import { useQueryParamState } from '@backstage/core'; import { SearchContextProvider } from '../SearchContext'; +import { JsonObject } from '@backstage/config'; export const SearchPageNext = () => { + const location = useLocation(); + const [queryString] = useQueryParamState('query'); + const filters = (qs.parse(location.search.substring(1), { arrayLimit: 0 }) + .filters || {}) as JsonObject; + const initialState = { + term: queryString || '', + types: [], + pageCursor: '', + filters, + }; + return ( - + ); diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 597bfabfb1..b02561c710 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -15,6 +15,7 @@ */ export * from './Filters'; +export * from './SearchFilterNext'; export * from './SearchFiltersNext'; export * from './SearchBar'; export * from './SearchBarNext'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 4d8d9c08a2..5ff047e986 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -22,7 +22,6 @@ export { SearchPageNext, SearchBarNext, SearchResultNext, - SearchFiltersNext, DefaultResultListItem, } from './plugin'; export { @@ -35,6 +34,7 @@ export { SearchContextProvider, useSearch, SearchPage as Router, + SearchFilterNext, SearchResult, SidebarSearch, } from './components'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index cff594b299..6ff40ebaf7 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -100,12 +100,3 @@ export const DefaultResultListItem = searchPlugin.provide( }, }), ); - -export const SearchFiltersNext = searchPlugin.provide( - createComponentExtension({ - component: { - lazy: () => - import('./components/SearchFiltersNext').then(m => m.SearchFiltersNext), - }, - }), -); From 54cdabdd3533172ae9ab4f0c18678595603e4252 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 24 May 2021 12:47:05 +0200 Subject: [PATCH 26/56] delete search filters component Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../SearchFiltersNext/FiltersButtonNext.tsx | 56 ------ .../SearchFiltersNext/SearchFiltersNext.tsx | 183 ------------------ .../components/SearchFiltersNext/index.tsx | 23 --- plugins/search/src/index.ts | 3 - 4 files changed, 265 deletions(-) delete mode 100644 plugins/search/src/components/SearchFiltersNext/FiltersButtonNext.tsx delete mode 100644 plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx delete mode 100644 plugins/search/src/components/SearchFiltersNext/index.tsx diff --git a/plugins/search/src/components/SearchFiltersNext/FiltersButtonNext.tsx b/plugins/search/src/components/SearchFiltersNext/FiltersButtonNext.tsx deleted file mode 100644 index f7628714ca..0000000000 --- a/plugins/search/src/components/SearchFiltersNext/FiltersButtonNext.tsx +++ /dev/null @@ -1,56 +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 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 FiltersButtonNext = ({ - numberOfSelectedFilters, - handleToggleFilters, -}: FiltersButtonProps) => { - const classes = useStyles(); - - return ( -
- - - - - Filters ({numberOfSelectedFilters ? numberOfSelectedFilters : 0}) - -
- ); -}; diff --git a/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx b/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx deleted file mode 100644 index d94b8021ef..0000000000 --- a/plugins/search/src/components/SearchFiltersNext/SearchFiltersNext.tsx +++ /dev/null @@ -1,183 +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 { - Card, - CardContent, - Checkbox, - FormControl, - InputLabel, - List, - MenuItem, - ListItem, - ListItemText, - makeStyles, - Select, -} from '@material-ui/core'; -import React from 'react'; -import { useSearch } from '../SearchContext'; - -const useFilterStyles = makeStyles({ - filters: { - background: 'transparent', - boxShadow: '0px 0px 0px 0px', - }, -}); - -const useCheckBoxStyles = makeStyles(theme => ({ - checkbox: { - padding: theme.spacing(0, 1, 0, 1), - }, -})); - -const useSelectStyles = makeStyles({ - select: { - width: '100%', - }, -}); - -export type FilterOptions = { - kind: Array; - lifecycle: Array; -}; - -type FiltersProps = { - children: React.ReactChild; -}; - -type ValuedFilterProps = { - fieldName: string; - values: string[]; -}; - -export enum FilterType { - CHECKBOX = 'checkbox', - SELECT = 'select', -} - -export type NewFilterDefinition = { - component: any; - props: any; -}; - -export type FilterDefinition = { - field: string; - type: FilterType; - values: string[]; -}; - -export const CheckBoxFilter = ({ fieldName, values }: ValuedFilterProps) => { - const { filters, setFilters } = useSearch(); - const classes = useCheckBoxStyles(); - - const setCheckboxFilter = (filter: string) => { - const newFilters = filters; - const currentValues = newFilters[fieldName] as string[]; - - if (!filter) return; - - if (!currentValues) { - setFilters({ ...filters, [fieldName]: [filter] }); - } else if (!currentValues?.includes(filter)) { - setFilters({ - ...filters, - [fieldName]: [...currentValues, filter], - }); - } else { - const filterToDelete = currentValues.find(value => value === filter); - if (filterToDelete) { - currentValues.splice(currentValues.indexOf(filterToDelete), 1); - - setFilters({ ...filters, [fieldName]: currentValues }); - } - } - }; - return ( - - {fieldName} - - {values.map((value: string) => ( - setCheckboxFilter(value)} - > - - - - ))} - - - ); -}; - -export const SelectFilter = ({ fieldName, values }: ValuedFilterProps) => { - const { filters, setFilters } = useSearch(); - const classes = useSelectStyles(); - - const setSelectFilter = (filter: string) => { - const newFilters = filters; - if (newFilters[fieldName] && filter === '') { - delete newFilters[fieldName]; - setFilters({ newFilters }); - } else { - setFilters({ ...filters, [fieldName]: filter as string }); - } - }; - - return ( - - - {fieldName} - - - - ); -}; - -export const SearchFiltersNext = ({ children }: FiltersProps) => { - const classes = useFilterStyles(); - - return {children}; -}; diff --git a/plugins/search/src/components/SearchFiltersNext/index.tsx b/plugins/search/src/components/SearchFiltersNext/index.tsx deleted file mode 100644 index dd35bd3624..0000000000 --- a/plugins/search/src/components/SearchFiltersNext/index.tsx +++ /dev/null @@ -1,23 +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 { FiltersButtonNext } from './FiltersButtonNext'; -export { - SearchFiltersNext, - CheckBoxFilter, - SelectFilter, - FilterType, -} from './SearchFiltersNext'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 5ff047e986..27fd1706fc 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -27,9 +27,6 @@ export { export { Filters, FiltersButton, - FilterType, - CheckBoxFilter, - SelectFilter, SearchBar, SearchContextProvider, useSearch, From d92b8ec5eaf481a8c3cc183fd7a9dcfdd48d3032 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 24 May 2021 15:40:57 +0200 Subject: [PATCH 27/56] fixups Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- plugins/search/src/components/SearchBarNext/SearchBarNext.tsx | 2 +- plugins/search/src/components/index.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index a36393a376..70868f5ce5 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { useDebounce } from 'react-use'; import { useQueryParamState } from '@backstage/core'; import { Paper, InputBase, IconButton, makeStyles } from '@material-ui/core'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index b02561c710..92e24b60df 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -16,7 +16,6 @@ export * from './Filters'; export * from './SearchFilterNext'; -export * from './SearchFiltersNext'; export * from './SearchBar'; export * from './SearchBarNext'; export * from './SearchPage'; From b264028394c27f4e2a8b459e4572b36456f2a04b Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 24 May 2021 18:27:00 +0200 Subject: [PATCH 28/56] initial tests Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../SearchBarNext/SearchBarNext.test.tsx | 29 +++++++++ .../SearchContext/SearchContext.test.tsx | 61 +++++++++++++++++++ .../SearchFilterNext.test.tsx | 37 +++++++++++ 3 files changed, 127 insertions(+) create mode 100644 plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx create mode 100644 plugins/search/src/components/SearchContext/SearchContext.test.tsx create mode 100644 plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx new file mode 100644 index 0000000000..3a5cb3e97a --- /dev/null +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { SearchBarNext } from './SearchBarNext'; + +describe('', () => { + it('renders without exploding', async () => { + const { getByRole } = await renderInTestApp(); + + expect( + getByRole('textbox', { name: 'search backstage' }), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/search/src/components/SearchContext/SearchContext.test.tsx b/plugins/search/src/components/SearchContext/SearchContext.test.tsx new file mode 100644 index 0000000000..cce315f62b --- /dev/null +++ b/plugins/search/src/components/SearchContext/SearchContext.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import * as SearchContext from './SearchContext'; + +const mockContextState = ({ term }: { term: string }) => { + return { + term, + pageCursor: '', + filters: {}, + types: ['*'], + result: { results: [], loading: false, error: undefined }, + setTerm: jest.fn(), + setFilters: jest.fn(), + setTypes: jest.fn(), + setPageCursor: jest.fn(), + }; +}; + +const MockSearchContextConsumer = () => { + const { term } = SearchContext.useSearch(); + + return
{term}
; +}; + +describe('useSearch', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('context should use initial term', async () => { + jest.spyOn(SearchContext, 'useSearch'); + const { getByRole } = await renderInTestApp(); + expect(getByRole('heading')).toBeInTheDocument(); + }); + + it('context should use mocked term', async () => { + jest + .spyOn(SearchContext, 'useSearch') + .mockImplementation(() => mockContextState({ term: 'new-term' })); + + const { getByRole } = await renderInTestApp(); + + expect(getByRole('heading', { name: 'new-term' })).toBeInTheDocument(); + }); +}); diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx new file mode 100644 index 0000000000..63822b9e00 --- /dev/null +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { SearchFilterNext } from './SearchFilterNext'; + +const MockFilterComponent = ({ name }: { name: string }) => { + return
{name}
; +}; + +describe('', () => { + it('renders without exploding', async () => { + const props = { + name: 'filter name', + }; + + const { getByRole } = await renderInTestApp( + , + ); + + expect(getByRole('heading', { name: 'filter name' })).toBeInTheDocument(); + }); +}); From 4e6d888340d0b625ccd5360f934623e543a57c51 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 May 2021 10:21:11 +0200 Subject: [PATCH 29/56] Move react type from devDeps to deps Signed-off-by: Eric Peterson --- plugins/search/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/search/package.json b/plugins/search/package.json index 0c99e9b81c..a02beefb4d 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", "qs": "^6.9.4", "react": "^16.13.1", "react-dom": "^16.13.1", From c70139e732190be37155cff424153203f57f8a88 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 May 2021 11:20:24 +0200 Subject: [PATCH 30/56] Update CheckBox filter defaultValue to be string array. Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchFilterNext/SearchFilterNext.tsx | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx index 02b625bd42..dd805c3af1 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx @@ -43,12 +43,9 @@ export type Props = { debug?: boolean; name: string; values?: string[]; - defaultValue?: string | null; + defaultValue?: string[] | string | null; }; -/** - * @param defaultValue - The default value. If more than one value should be defaulted to "checked," pass a comma-separated string. - */ const CheckboxFilter = ({ name, defaultValue, values }: Component) => { const { filters, setFilters } = useSearch(); @@ -76,13 +73,12 @@ const CheckboxFilter = ({ name, defaultValue, values }: Component) => { }; useEffect(() => { - if (defaultValue) { + if (defaultValue && Array.isArray(defaultValue)) { setFilters(prevFilters => ({ ...prevFilters, - [name]: defaultValue.split(','), + [name]: defaultValue, })); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue, setFilters]); return ( @@ -127,18 +123,14 @@ const SelectFilter = ({ name, defaultValue, values }: Component) => { } }; - useEffect( - () => { - if (defaultValue) { - setFilters(prevFilters => ({ - ...prevFilters, - [name]: defaultValue, - })); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [setFilters, defaultValue], - ); + useEffect(() => { + if (defaultValue && typeof defaultValue === 'string') { + setFilters(prevFilters => ({ + ...prevFilters, + [name]: defaultValue, + })); + } + }, [setFilters, defaultValue]); return ( From db6fd04d732c68e742bdcdc980926990caf6ee69 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 May 2021 14:36:58 +0200 Subject: [PATCH 31/56] Move query param logic from SearchBar to SearchPage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchBarNext/SearchBarNext.tsx | 32 ++++++++--------- .../SearchPageNext/SearchPageNext.tsx | 34 ++++++++++++++++--- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index 70868f5ce5..0e8c626b6f 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { useDebounce } from 'react-use'; -import { useQueryParamState } from '@backstage/core'; import { Paper, InputBase, IconButton, makeStyles } from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; @@ -32,23 +31,26 @@ const useStyles = makeStyles(() => ({ }, })); -export const SearchBarNext = () => { +type Props = { + debounceTime?: number; +}; + +export const SearchBarNext = ({ debounceTime = 200 }: Props) => { const classes = useStyles(); const { term, setTerm, setPageCursor } = useSearch(); - - const [, setQueryString] = useQueryParamState('query'); + const [value, setValue] = useState(term); useDebounce( () => { - setQueryString(term); + setTerm(value); }, - 200, - [term], + debounceTime, + [value], ); const handleSearch = (event: React.ChangeEvent | React.FormEvent) => { event.preventDefault(); - setTerm((event.target as HTMLInputElement).value as string); + setValue((event.target as HTMLInputElement).value as string); }; const handleClearSearchBar = () => { @@ -57,22 +59,18 @@ export const SearchBarNext = () => { }; return ( - handleSearch(e)} - className={classes.root} - > + handleSearch(e)} + value={value} + onChange={handleSearch} inputProps={{ 'aria-label': 'search backstage' }} /> - handleClearSearchBar()}> + diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index f3a4efc32c..bae5be6042 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -17,15 +17,38 @@ import React from 'react'; import qs from 'qs'; import { Outlet, useLocation } from 'react-router'; -import { useQueryParamState } from '@backstage/core'; -import { SearchContextProvider } from '../SearchContext'; +import { SearchContextProvider, useSearch } from '../SearchContext'; import { JsonObject } from '@backstage/config'; +export const UrlUpdater = () => { + const { term, types, pageCursor, filters } = useSearch(); + + const newParams = qs.stringify( + { + query: term, + types, + pageCursor, + filters, + }, + { arrayFormat: 'brackets' }, + ); + const newUrl = `${window.location.pathname}?${newParams}`; + + // We directly manipulate window history here in order to not re-render + // infinitely (state => location => state => etc). The intention of this + // code is just to ensure the right query/filters are loaded when a user + // clicks the "back" button after clicking a result. + window.history.replaceState(null, document.title, newUrl); + + return null; +}; + export const SearchPageNext = () => { const location = useLocation(); - const [queryString] = useQueryParamState('query'); - const filters = (qs.parse(location.search.substring(1), { arrayLimit: 0 }) - .filters || {}) as JsonObject; + const query = qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {}; + const filters = (query.filters as JsonObject) || {}; + const queryString = (query.query as string) || ''; + const initialState = { term: queryString || '', types: [], @@ -35,6 +58,7 @@ export const SearchPageNext = () => { return ( + ); From 83a293b5f9d90287cd54b9ef900139dac34eac92 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 May 2021 14:50:45 +0200 Subject: [PATCH 32/56] Clear cursorPage on the Context level Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../src/components/SearchBarNext/SearchBarNext.tsx | 3 +-- .../src/components/SearchContext/SearchContext.tsx | 11 ++++++++++- .../src/components/SearchPageNext/SearchPageNext.tsx | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index 0e8c626b6f..0a42bfd296 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -37,7 +37,7 @@ type Props = { export const SearchBarNext = ({ debounceTime = 200 }: Props) => { const classes = useStyles(); - const { term, setTerm, setPageCursor } = useSearch(); + const { term, setTerm } = useSearch(); const [value, setValue] = useState(term); useDebounce( @@ -55,7 +55,6 @@ export const SearchBarNext = ({ debounceTime = 200 }: Props) => { const handleClearSearchBar = () => { setTerm(''); - setPageCursor(''); }; return ( diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index ac264ccf32..40a9c78cf6 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -19,8 +19,9 @@ import React, { createContext, useContext, useState, + useEffect, } from 'react'; -import { useAsync } from 'react-use'; +import { useAsync, usePrevious } from 'react-use'; import { useApi } from '@backstage/core'; import { SearchResultSet } from '@backstage/search-common'; import { searchApiRef } from '../../apis'; @@ -60,6 +61,7 @@ export const SearchContextProvider = ({ const [filters, setFilters] = useState(initialState.filters); const [term, setTerm] = useState(initialState.term); const [types, setTypes] = useState(initialState.types); + const prevTerm = usePrevious(term); const result = useAsync( () => @@ -72,6 +74,13 @@ export const SearchContextProvider = ({ [term, filters, types, pageCursor], ); + useEffect(() => { + // Any time a term is reset, we want to start from page 0. + if (term && prevTerm && term !== prevTerm) { + setPageCursor(''); + } + }, [term, prevTerm]); + const value: SearchContextValue = { result, filters, diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index bae5be6042..cd069b8d2e 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -48,11 +48,12 @@ export const SearchPageNext = () => { const query = qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {}; const filters = (query.filters as JsonObject) || {}; const queryString = (query.query as string) || ''; + const pageCursor = (query.pageCursor as string) || ''; const initialState = { term: queryString || '', types: [], - pageCursor: '', + pageCursor, filters, }; From 8f3994141b2926ee5555928fc40494c0e5d694a3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 May 2021 14:57:52 +0200 Subject: [PATCH 33/56] Set default value for SearchBar as zero Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- packages/app/src/components/search/SearchPage.tsx | 2 +- plugins/search/src/components/SearchBarNext/SearchBarNext.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 8480edf514..3955ec1a2a 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -31,7 +31,7 @@ export const searchPage = ( - + diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index 0a42bfd296..014e3d6ee4 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -35,7 +35,7 @@ type Props = { debounceTime?: number; }; -export const SearchBarNext = ({ debounceTime = 200 }: Props) => { +export const SearchBarNext = ({ debounceTime = 0 }: Props) => { const classes = useStyles(); const { term, setTerm } = useSearch(); const [value, setValue] = useState(term); From a4c2bb02db7349143a5c2225314c2685b4f9d337 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 May 2021 15:24:10 +0200 Subject: [PATCH 34/56] Add key to Checkbox and Select filters Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchFilterNext/SearchFilterNext.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx index dd805c3af1..d745b5c2d4 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx @@ -79,30 +79,31 @@ const CheckboxFilter = ({ name, defaultValue, values }: Component) => { [name]: defaultValue, })); } - }, [defaultValue, setFilters]); + }, [name, defaultValue, setFilters]); return ( {name} {values && - values.map((v: string) => ( + values.map((value: string) => ( setCheckboxFilter(v)} + inputProps={{ 'aria-labelledby': value }} + value={value} + name={value} + onChange={() => setCheckboxFilter(value)} checked={ filters[name] - ? (filters[name] as string[]).includes(v) + ? (filters[name] as string[]).includes(value) : false } /> } - label={v} + label={value} /> ))} @@ -130,7 +131,7 @@ const SelectFilter = ({ name, defaultValue, values }: Component) => { [name]: defaultValue, })); } - }, [setFilters, defaultValue]); + }, [name, defaultValue, setFilters]); return ( @@ -147,7 +148,9 @@ const SelectFilter = ({ name, defaultValue, values }: Component) => { {values && values.map((value: string) => ( - {value} + + {value} + ))} From f644678bd84508e30ba31b8ed56194fd3b39e0d6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 May 2021 19:49:02 +0200 Subject: [PATCH 35/56] :recycle: Reduce setFilters logic of Checkbox and Select Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchFilterNext/SearchFilterNext.tsx | 158 ++++++++---------- 1 file changed, 72 insertions(+), 86 deletions(-) diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx index d745b5c2d4..0c13fbc008 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ReactElement, useEffect } from 'react'; + +import React, { ReactElement, ChangeEvent, useEffect } from 'react'; import { makeStyles, FormControl, @@ -28,52 +29,28 @@ import { import { useSearch } from '../SearchContext'; const useStyles = makeStyles({ - select: { - width: '100%', - }, - subtitle: { + label: { textTransform: 'capitalize', }, }); -export type Component = Omit; - -export type Props = { - component: (props: Component) => ReactElement; - debug?: boolean; +export type Component = { name: string; values?: string[]; defaultValue?: string[] | string | null; }; -const CheckboxFilter = ({ name, defaultValue, values }: Component) => { +export type Props = Component & { + component: (props: Component) => ReactElement; + debug?: boolean; +}; + +const CheckboxFilter = ({ name, defaultValue, values = [] }: Component) => { + const classes = useStyles(); const { filters, setFilters } = useSearch(); - const setCheckboxFilter = (filter: string) => { - const newFilters = filters; - const currentValues = newFilters[name] as string[]; - - if (!filter) return; - - if (!currentValues) { - setFilters({ ...filters, [name]: [filter] }); - } else if (!currentValues?.includes(filter)) { - setFilters({ - ...filters, - [name]: [...currentValues, filter], - }); - } else { - const filterToDelete = currentValues.find(value => value === filter); - if (filterToDelete) { - currentValues.splice(currentValues.indexOf(filterToDelete), 1); - - setFilters({ ...filters, [name]: currentValues }); - } - } - }; - useEffect(() => { - if (defaultValue && Array.isArray(defaultValue)) { + if (Array.isArray(defaultValue)) { setFilters(prevFilters => ({ ...prevFilters, [name]: defaultValue, @@ -81,51 +58,49 @@ const CheckboxFilter = ({ name, defaultValue, values }: Component) => { } }, [name, defaultValue, setFilters]); + const handleChange = (e: ChangeEvent) => { + const { + target: { value, checked }, + } = e; + + setFilters(prevFilters => { + const { [name]: filter, ...others } = prevFilters; + const rest = ((filter as string[]) || []).filter(i => i !== value); + const items = checked ? [...rest, value] : rest; + return items.length ? { ...others, [name]: items } : others; + }); + }; + return ( - {name} - {values && - values.map((value: string) => ( - setCheckboxFilter(value)} - checked={ - filters[name] - ? (filters[name] as string[]).includes(value) - : false - } - /> - } - label={value} - /> - ))} + {name} + {values.map((value: string) => ( + + } + label={value} + /> + ))} ); }; -const SelectFilter = ({ name, defaultValue, values }: Component) => { +const SelectFilter = ({ name, defaultValue, values = [] }: Component) => { const classes = useStyles(); const { filters, setFilters } = useSearch(); - const setSelectFilter = (filter: string) => { - const newFilters = filters; - if (newFilters[name] && filter === '') { - delete newFilters[name]; - setFilters({ newFilters }); - } else { - setFilters({ ...filters, [name]: filter as string }); - } - }; - useEffect(() => { - if (defaultValue && typeof defaultValue === 'string') { + if (typeof defaultValue === 'string') { setFilters(prevFilters => ({ ...prevFilters, [name]: defaultValue, @@ -133,38 +108,49 @@ const SelectFilter = ({ name, defaultValue, values }: Component) => { } }, [name, defaultValue, setFilters]); + const handleChange = (e: ChangeEvent<{ value: unknown }>) => { + const { + target: { value }, + } = e; + + setFilters(prevFilters => { + const { [name]: filter, ...others } = prevFilters; + return value ? { ...others, [name]: value as string } : others; + }); + }; + return ( - - {name} + + + {name} + ); }; -const SearchFilterNext = ({ component: Element, ...props }: Props) => { - return ; -}; +const SearchFilterNext = ({ component: Element, ...props }: Props) => ( + +); -SearchFilterNext.Checkbox = (props: Omit) => ( +SearchFilterNext.Checkbox = (props: Omit & Component) => ( ); -SearchFilterNext.Select = (props: Omit) => ( + +SearchFilterNext.Select = (props: Omit & Component) => ( ); From 56d84c51a949ec5ba15fe60b20a15fd654e62e5b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 26 May 2021 11:01:21 +0200 Subject: [PATCH 36/56] =?UTF-8?q?chore(plugins/search):=20=E2=9E=95=20@tes?= =?UTF-8?q?ting-library/react-hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- plugins/search/package.json | 1 + yarn.lock | 57 +++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/plugins/search/package.json b/plugins/search/package.json index a02beefb4d..f2c10b89c4 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -52,6 +52,7 @@ "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^7.0.0", "@testing-library/user-event": "^13.1.8", "@types/react": "^16.9", "@types/jest": "^26.0.7", diff --git a/yarn.lock b/yarn.lock index 25202523c2..a5832e82da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5700,6 +5700,17 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.4.0" +"@testing-library/react-hooks@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.0.tgz#dd6d37a7e018f147a3b9153137f10e013be8472b" + integrity sha512-WFBGH8DWdIGGBHt6PBtQPe2v4Kbj9vQ1sQ9qLBTmwn1PNggngint4MTE/IiWCYhPbyTW3oc/7X62DObMn/AjQQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + react-error-boundary "^3.1.0" + "@testing-library/react@^11.2.5": version "11.2.6" resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" @@ -6667,6 +6678,13 @@ "@types/webpack" "^4" "@types/webpack-dev-server" "*" +"@types/react-dom@>=16.9.0": + version "17.0.5" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227" + integrity sha512-ikqukEhH4H9gr4iJCmQVNzTB307kROe3XFfHAOTxOXPOw7lAoEXnM5KWTkzeANGL5Ce6ABfiMl/zJBYNi7ObmQ== + dependencies: + "@types/react" "*" + "@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" @@ -6710,6 +6728,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@>=16.9.0": + version "17.0.1" + resolved "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" + integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + dependencies: + "@types/react" "*" + "@types/react-text-truncate@^0.14.0": version "0.14.0" resolved "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.0.tgz#588bbabbc7f2a13815e805f3a48942db73fe65fe" @@ -6746,6 +6771,15 @@ dependencies: csstype "^2.2.0" +"@types/react@>=16.9.0": + version "17.0.8" + resolved "https://registry.npmjs.org/@types/react/-/react-17.0.8.tgz#fe76e3ba0fbb5602704110fd1e3035cf394778e3" + integrity sha512-3sx4c0PbXujrYAKwXxNONXUtRp9C+hE2di0IuxFyf5BELD+B+AXL8G7QrmSKhVwKZDbv0igiAjQAMhXj8Yg3aw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -6827,6 +6861,11 @@ "@types/node" "*" rollup "^0.63.4" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/semver@^6.0.0": version "6.2.1" resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" @@ -14575,7 +14614,7 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@^2.8.2: version "2.8.2" resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== @@ -14585,7 +14624,7 @@ graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2. graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@^1.9.0: version "1.9.0" resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== @@ -22182,6 +22221,13 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" +react-error-boundary@^3.1.0: + version "3.1.3" + resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz#276bfa05de8ac17b863587c9e0647522c25e2a0b" + integrity sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.7, react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" @@ -25955,11 +26001,16 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: +typescript@^4.0.3, typescript@^4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== +typescript@~4.1.3: + version "4.1.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" + integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From 4e97c47c9390654e6a9d6232a368674e74ae7a1c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 26 May 2021 11:04:47 +0200 Subject: [PATCH 37/56] test(plugins/search): complete coverage of SearchContext Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchBarNext/SearchBarNext.test.tsx | 32 ++- .../SearchContext/SearchContext.test.tsx | 214 +++++++++++++++--- .../SearchContext/SearchContext.tsx | 2 +- 3 files changed, 218 insertions(+), 30 deletions(-) diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx index 3a5cb3e97a..f29cc53b71 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx @@ -16,11 +16,41 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; +import { useApi } from '@backstage/core'; + +import { SearchContextProvider } from '../SearchContext'; import { SearchBarNext } from './SearchBarNext'; +jest.mock('@backstage/core', () => ({ + ...jest.requireActual('@backstage/core'), + useApi: jest.fn(), +})); + describe('', () => { + const _alphaPerformSearch = jest.fn(); + + const initialState = { + term: '', + pageCursor: '', + filters: {}, + types: ['*'], + }; + + beforeEach(() => { + _alphaPerformSearch.mockResolvedValue([]); + (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + it('renders without exploding', async () => { - const { getByRole } = await renderInTestApp(); + const { getByRole } = await renderInTestApp( + + + , + ); expect( getByRole('textbox', { name: 'search backstage' }), diff --git a/plugins/search/src/components/SearchContext/SearchContext.test.tsx b/plugins/search/src/components/SearchContext/SearchContext.test.tsx index cce315f62b..c45d74eade 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.test.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.test.tsx @@ -15,47 +15,205 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import * as SearchContext from './SearchContext'; +import { render, screen, waitFor } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; -const mockContextState = ({ term }: { term: string }) => { - return { - term, +import { useApi } from '@backstage/core'; + +import { useSearch, SearchContextProvider } from './SearchContext'; + +jest.mock('@backstage/core', () => ({ + ...jest.requireActual('@backstage/core'), + useApi: jest.fn(), +})); + +describe('SearchContext', () => { + const _alphaPerformSearch = jest.fn(); + + const wrapper = ({ children, initialState }: any) => ( + + {children} + + ); + + const initialState = { + term: '', pageCursor: '', filters: {}, types: ['*'], - result: { results: [], loading: false, error: undefined }, - setTerm: jest.fn(), - setFilters: jest.fn(), - setTypes: jest.fn(), - setPageCursor: jest.fn(), }; -}; -const MockSearchContextConsumer = () => { - const { term } = SearchContext.useSearch(); + beforeEach(() => { + _alphaPerformSearch.mockResolvedValue([]); + (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); + }); - return
{term}
; -}; - -describe('useSearch', () => { - afterEach(() => { + afterAll(() => { jest.resetAllMocks(); }); - it('context should use initial term', async () => { - jest.spyOn(SearchContext, 'useSearch'); - const { getByRole } = await renderInTestApp(); - expect(getByRole('heading')).toBeInTheDocument(); + it('Passes children', async () => { + const text = 'text'; + + render( + + {text} + , + ); + + await waitFor(() => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); }); - it('context should use mocked term', async () => { - jest - .spyOn(SearchContext, 'useSearch') - .mockImplementation(() => mockContextState({ term: 'new-term' })); + it('Throws error when no context is set', () => { + const { result } = renderHook(() => useSearch()); - const { getByRole } = await renderInTestApp(); + expect(result.error).toEqual( + Error('useSearch must be used within a SearchContextProvider'), + ); + }); - expect(getByRole('heading', { name: 'new-term' })).toBeInTheDocument(); + it('Uses initial state values', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + }, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual(expect.objectContaining(initialState)); + }); + + it('Resets cursor when term is set (and different from previous)', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState: { + ...initialState, + pageCursor: '1', + }, + }, + }); + + await waitForNextUpdate(); + + expect(result.current.pageCursor).toBe('1'); + + act(() => { + result.current.setTerm('first term'); + }); + + await waitForNextUpdate(); + + expect(result.current.pageCursor).toBe('1'); + + act(() => { + result.current.setTerm('second term'); + }); + + await waitForNextUpdate(); + + expect(result.current.pageCursor).toBe(''); + }); + + describe('Performs search (and sets results)', () => { + it('When term is set', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + }, + }); + + await waitForNextUpdate(); + + const term = 'term'; + + act(() => { + result.current.setTerm(term); + }); + + await waitForNextUpdate(); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith({ + ...initialState, + term, + }); + }); + + it('When filters are set', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + }, + }); + + await waitForNextUpdate(); + + const filters = { filter: 'filter' }; + + act(() => { + result.current.setFilters(filters); + }); + + await waitForNextUpdate(); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith({ + ...initialState, + filters, + }); + }); + + it('When pageCursor is set', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + }, + }); + + await waitForNextUpdate(); + + const pageCursor = 'pageCursor'; + + act(() => { + result.current.setPageCursor(pageCursor); + }); + + await waitForNextUpdate(); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith({ + ...initialState, + pageCursor, + }); + }); + + it('When types is set', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSearch(), { + wrapper, + initialProps: { + initialState, + }, + }); + + await waitForNextUpdate(); + + const types = ['type']; + + act(() => { + result.current.setTypes(types); + }); + + await waitForNextUpdate(); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith({ + ...initialState, + types, + }); + }); }); }); diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index 40a9c78cf6..55ccd096a1 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -45,7 +45,7 @@ type SettableSearchContext = Omit< 'result' | 'setTerm' | 'setTypes' | 'setFilters' | 'setPageCursor' >; -const SearchContext = createContext({} as SearchContextValue); +const SearchContext = createContext(undefined); export const SearchContextProvider = ({ initialState = { From 6ac8da4d49ef4ab6643d0dfa56799a08e8214fba Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 26 May 2021 12:18:42 +0200 Subject: [PATCH 38/56] Adding test coverage for SearchPageNext. Signed-off-by: Eric Peterson --- .../SearchPageNext/SearchPageNext.test.tsx | 105 ++++++++++++++++++ .../SearchPageNext/SearchPageNext.tsx | 3 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx new file mode 100644 index 0000000000..f00772f1c5 --- /dev/null +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import { useLocation, Outlet } from 'react-router'; + +import { useSearch, SearchContextProvider } from '../SearchContext'; +import { SearchPageNext } from './'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useLocation: jest.fn().mockReturnValue({ + search: '', + }), + Outlet: jest.fn().mockReturnValue(null), +})); + +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + SearchContextProvider: jest + .fn() + .mockImplementation(({ children }) => children), + useSearch: jest.fn().mockReturnValue({ + term: '', + types: [], + filters: {}, + pageCursor: '', + }), +})); + +describe('SearchPage', () => { + const origReplaceState = window.history.replaceState; + + beforeEach(() => { + window.history.replaceState = jest.fn(); + }); + + afterEach(() => { + window.history.replaceState = origReplaceState; + }); + + it('uses initial term state from location', async () => { + // Given this initial location.search value... + const expectedFilterField = 'anyKey'; + const expectedFilterValue = 'anyValue'; + const expectedTerm = 'justin bieber'; + const expectedTypes = ['software-catalog']; + const expectedFilters = { [expectedFilterField]: expectedFilterValue }; + const expectedPageCursor = 'page2-or-something'; + + // e.g. ?query=petstore&pageCursor=1&filters[lifecycle][]=experimental&filters[kind]=Component + (useLocation as jest.Mock).mockReturnValueOnce({ + search: `?query=${expectedTerm}&types[]=${expectedTypes[0]}&filters[${expectedFilterField}]=${expectedFilterValue}&pageCursor=${expectedPageCursor}`, + }); + + // When we render the page... + await renderInTestApp(); + + // Then search context should be initialized with these values... + const calls = (SearchContextProvider as jest.Mock).mock.calls[0]; + const actualInitialState = calls[0].initialState; + expect(actualInitialState.term).toEqual(expectedTerm); + expect(actualInitialState.types).toEqual(expectedTypes); + expect(actualInitialState.pageCursor).toEqual(expectedPageCursor); + expect(actualInitialState.filters).toStrictEqual(expectedFilters); + }); + + it('renders provided router element', async () => { + await renderInTestApp(); + + expect(Outlet).toHaveBeenCalled(); + }); + + it('replaces window history with expected query parameters', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + term: 'bieber', + types: ['software-catalog'], + pageCursor: 'page2-or-something', + filters: { anyKey: 'anyValue' }, + }); + const expectedLocation = encodeURI( + '?query=bieber&types[]=software-catalog&pageCursor=page2-or-something&filters[anyKey]=anyValue', + ); + + await renderInTestApp(); + + const calls = (window.history.replaceState as jest.Mock).mock.calls[0]; + expect(calls[2]).toContain(expectedLocation); + }); +}); diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index cd069b8d2e..f851778ddf 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -49,10 +49,11 @@ export const SearchPageNext = () => { const filters = (query.filters as JsonObject) || {}; const queryString = (query.query as string) || ''; const pageCursor = (query.pageCursor as string) || ''; + const types = (query.types as string[]) || []; const initialState = { term: queryString || '', - types: [], + types, pageCursor, filters, }; From f131c3de21e52b6d0ddeb0985a579db99179ef67 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 26 May 2021 12:22:51 +0200 Subject: [PATCH 39/56] Fix Lunr tests to match new translation logic. Signed-off-by: Eric Peterson --- .../src/engines/LunrSearchEngine.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index 6c348235e0..f597327dbf 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -53,7 +53,7 @@ describe('LunrSearchEngine', () => { expect(mockedTranslatedQuery).toMatchObject({ documentTypes: ['*'], - lunrQueryString: 'testTerm', + lunrQueryString: '+testTerm', }); }); @@ -66,7 +66,7 @@ describe('LunrSearchEngine', () => { expect(mockedTranslatedQuery).toMatchObject({ documentTypes: ['*'], - lunrQueryString: 'testTerm +kind:testKind', + lunrQueryString: '+testTerm +kind:testKind', }); }); @@ -79,7 +79,7 @@ describe('LunrSearchEngine', () => { expect(mockedTranslatedQuery).toMatchObject({ documentTypes: ['*'], - lunrQueryString: 'testTerm +kind:testKind +namespace:testNameSpace', + lunrQueryString: '+testTerm +kind:testKind +namespace:testNameSpace', }); }); }); From 970082195c0fe1b5e706648790e9c503a2a53ab1 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 26 May 2021 13:56:41 +0200 Subject: [PATCH 40/56] test(plugins/search): complete SearchResultNext coverage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchContext/SearchContext.test.tsx | 2 +- .../SearchPageNext/SearchPageNext.test.tsx | 1 - .../SearchResultNext.test.tsx | 92 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 plugins/search/src/components/SearchResultNext/SearchResultNext.test.tsx diff --git a/plugins/search/src/components/SearchContext/SearchContext.test.tsx b/plugins/search/src/components/SearchContext/SearchContext.test.tsx index c45d74eade..1b79e62698 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.test.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.test.tsx @@ -44,7 +44,7 @@ describe('SearchContext', () => { }; beforeEach(() => { - _alphaPerformSearch.mockResolvedValue([]); + _alphaPerformSearch.mockResolvedValue({}); (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); }); diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx index f00772f1c5..5890f8cf41 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.test.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; -import { render, screen, waitFor } from '@testing-library/react'; import { useLocation, Outlet } from 'react-router'; import { useSearch, SearchContextProvider } from '../SearchContext'; diff --git a/plugins/search/src/components/SearchResultNext/SearchResultNext.test.tsx b/plugins/search/src/components/SearchResultNext/SearchResultNext.test.tsx new file mode 100644 index 0000000000..98fc285265 --- /dev/null +++ b/plugins/search/src/components/SearchResultNext/SearchResultNext.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 { render, waitFor } from '@testing-library/react'; + +import { SearchResultNext } from './SearchResultNext'; +import { useSearch } from '../SearchContext'; + +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchResultNext', () => { + it('Progress rendered on Loading state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: true }, + }); + + const { getByRole } = render( + {() => <>}, + ); + + await waitFor(() => { + expect(getByRole('progressbar')).toBeInTheDocument(); + }); + }); + + it('Alert rendered on Error state', async () => { + const error = 'error'; + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error }, + }); + + const { getByRole } = render( + {() => <>}, + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + `Error encountered while fetching search results. ${error}`, + ); + }); + }); + + it('On empty result value state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error: '', value: undefined }, + }); + + const { getByRole } = render( + {() => <>}, + ); + + await waitFor(() => { + expect( + getByRole('heading', { name: 'Sorry, no results were found' }), + ).toBeInTheDocument(); + }); + }); + + it('Calls children with results set to result.value', () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error: '', value: { results: [] } }, + }); + + render( + + {({ results }) => { + expect(results).toEqual([]); + return <>; + }} + , + ); + }); +}); From 51e2b40cbb354569afeddd8e665705f0fb8cd84d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 26 May 2021 17:29:36 +0200 Subject: [PATCH 41/56] test(plugins/search): complete SearchFilterNext coverage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchFilterNext.test.tsx | 362 +++++++++++++++++- 1 file changed, 349 insertions(+), 13 deletions(-) diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx index 63822b9e00..ed9c0ecd74 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx @@ -15,23 +15,359 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { screen, render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useApi } from '@backstage/core'; + import { SearchFilterNext } from './SearchFilterNext'; +import { SearchContextProvider } from '../SearchContext'; -const MockFilterComponent = ({ name }: { name: string }) => { - return
{name}
; -}; +jest.mock('@backstage/core', () => ({ + ...jest.requireActual('@backstage/core'), + useApi: jest.fn().mockReturnValue({ + _alphaPerformSearch: jest.fn().mockResolvedValue({}), + }), +})); -describe('', () => { - it('renders without exploding', async () => { - const props = { - name: 'filter name', - }; +describe('SearchFilterNext', () => { + const initialState = { + term: '', + filters: {}, + types: [], + pageCursor: '', + }; - const { getByRole } = await renderInTestApp( - , - ); + const name = 'field'; + const values = ['value1', 'value2']; + const filters = { unrelated: 'unrelated' }; - expect(getByRole('heading', { name: 'filter name' })).toBeInTheDocument(); + const _alphaPerformSearch = jest.fn().mockResolvedValue({}); + (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); + + it('Check that element was rendered and received props', async () => { + const CustomFilter = (props: { name: string }) =>
{props.name}
; + + render(); + + expect(screen.getByRole('heading', { name })).toBeInTheDocument(); + }); + + describe('Checkbox', () => { + it('Renders field name and values when provided as props', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).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( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).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( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).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( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + const checkBox = screen.getByRole('checkbox', { name: values[0] }); + + // Check the box. + userEvent.click(checkBox); + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ filters: { field: [values[0]] } }), + ); + }); + + // Uncheck the box. + userEvent.click(checkBox); + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ filters: {} }), + ); + }); + }); + + it('Checking / unchecking a value maintains unrelated filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + const checkBox = screen.getByRole('checkbox', { name: values[0] }); + + // Check the box. + userEvent.click(checkBox); + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { ...filters, field: [values[0]] }, + }), + ); + }); + + // Uncheck the box. + userEvent.click(checkBox); + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ filters }), + ); + }); + }); + }); + + describe('Select', () => { + it('Renders field name and values when provided as props', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + 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( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + 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( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + 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( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + const button = screen.getByRole('button'); + + userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByRole('option', { name: values[0] })); + + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { [name]: values[0] }, + }), + ); + }); + + userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByRole('option', { name: 'All' })); + + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: {}, + }), + ); + }); + }); + + it('Selecting a value maintains unrelated filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(name)).toBeInTheDocument(); + }); + + const button = screen.getByRole('button'); + + userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByRole('option', { name: values[0] })); + + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { ...filters, [name]: values[0] }, + }), + ); + }); + + userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByRole('option', { name: 'All' })); + + await waitFor(() => { + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ filters }), + ); + }); + }); }); }); From c568e721f4d898c9abcd9a940341f27240ed43f6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 26 May 2021 21:11:35 +0200 Subject: [PATCH 42/56] test(plugins/search): complete SearchBarNext coverage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../SearchBarNext/SearchBarNext.test.tsx | 122 ++++++++++++++++-- .../SearchBarNext/SearchBarNext.tsx | 46 +++---- .../SearchFilterNext.test.tsx | 8 +- 3 files changed, 137 insertions(+), 39 deletions(-) diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx index f29cc53b71..a5075c5593 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.test.tsx @@ -15,7 +15,8 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { screen, render, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useApi } from '@backstage/core'; import { SearchContextProvider } from '../SearchContext'; @@ -23,12 +24,10 @@ import { SearchBarNext } from './SearchBarNext'; jest.mock('@backstage/core', () => ({ ...jest.requireActual('@backstage/core'), - useApi: jest.fn(), + useApi: jest.fn().mockReturnValue({}), })); -describe('', () => { - const _alphaPerformSearch = jest.fn(); - +describe('SearchBarNext', () => { const initialState = { term: '', pageCursor: '', @@ -36,24 +35,119 @@ describe('', () => { types: ['*'], }; - beforeEach(() => { - _alphaPerformSearch.mockResolvedValue([]); - (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); - }); + const name = 'Search term'; + const term = 'term'; + + const _alphaPerformSearch = jest.fn().mockResolvedValue({}); + (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); afterAll(() => { jest.resetAllMocks(); }); - it('renders without exploding', async () => { - const { getByRole } = await renderInTestApp( + it('Renders without exploding', async () => { + render( , ); - expect( - getByRole('textbox', { name: 'search backstage' }), - ).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + }); + + it('Renders based on initial search', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(term); + }); + }); + + it('Updates term state when text is entered', async () => { + render( + + + , + ); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + userEvent.type(textbox, value); + + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + }); + + it('Clear button clears term state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(term); + }); + + userEvent.click(screen.getByRole('button', { name: 'Clear term' })); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(''); + }); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ term: '' }), + ); + }); + + it('Adheres to provided debounceTime', async () => { + jest.useFakeTimers(); + + const debounceTime = 600; + + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + userEvent.type(textbox, value); + + expect(_alphaPerformSearch).not.toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); + + expect(_alphaPerformSearch).toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); }); }); diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index 014e3d6ee4..b223e6c2cd 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -14,17 +14,26 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { ChangeEvent, useState } from 'react'; import { useDebounce } from 'react-use'; -import { Paper, InputBase, IconButton, makeStyles } from '@material-ui/core'; +import { + Theme, + Paper, + InputBase, + InputAdornment, + IconButton, + makeStyles, +} from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; + import { useSearch } from '../SearchContext'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme: Theme) => ({ root: { display: 'flex', alignItems: 'center', + padding: theme.spacing(0, 0, 0, 1.5), }, input: { flex: 1, @@ -40,36 +49,29 @@ export const SearchBarNext = ({ debounceTime = 0 }: Props) => { const { term, setTerm } = useSearch(); const [value, setValue] = useState(term); - useDebounce( - () => { - setTerm(value); - }, - debounceTime, - [value], - ); + useDebounce(() => setTerm(value), debounceTime, [value]); - const handleSearch = (event: React.ChangeEvent | React.FormEvent) => { - event.preventDefault(); - setValue((event.target as HTMLInputElement).value as string); + const handleSearch = (e: ChangeEvent) => { + setValue(e.target.value); }; - const handleClearSearchBar = () => { - setTerm(''); - }; + const handleClear = () => setValue(''); return ( - - - - + + + + } /> - + diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx index ed9c0ecd74..1a35bf54d4 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.test.tsx @@ -24,9 +24,7 @@ import { SearchContextProvider } from '../SearchContext'; jest.mock('@backstage/core', () => ({ ...jest.requireActual('@backstage/core'), - useApi: jest.fn().mockReturnValue({ - _alphaPerformSearch: jest.fn().mockResolvedValue({}), - }), + useApi: jest.fn().mockReturnValue({}), })); describe('SearchFilterNext', () => { @@ -44,6 +42,10 @@ describe('SearchFilterNext', () => { const _alphaPerformSearch = jest.fn().mockResolvedValue({}); (useApi as jest.Mock).mockReturnValue({ _alphaPerformSearch }); + afterAll(() => { + jest.resetAllMocks(); + }); + it('Check that element was rendered and received props', async () => { const CustomFilter = (props: { name: string }) =>
{props.name}
; From 49df6413f37589b082bf624080e9780cbb5cb10f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 26 May 2021 21:56:27 +0200 Subject: [PATCH 43/56] test(plugins/search): complete DefaultResultListItem coverage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../DefaultResultListItem.test.jsx | 41 +++++++++++++++++++ .../DefaultResultListItem.tsx | 7 +++- .../SearchResultNext/SearchResultNext.tsx | 13 +++--- 3 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx new file mode 100644 index 0000000000..60ad453597 --- /dev/null +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx @@ -0,0 +1,41 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; + +import { DefaultResultListItem } from './DefaultResultListItem'; + +describe('DefaultResultListItem', () => { + const result = { + title: 'title', + text: 'text', + location: '/location', + }; + + it('Links to result.location', async () => { + await renderInTestApp(); + expect(screen.getByRole('link')).toHaveAttribute('href', result.location); + }); + + it('Includes primary/secondary text (title / text)', async () => { + await renderInTestApp(); + expect(screen.getByRole('listitem')).toHaveTextContent( + result.title + result.text, + ); + }); +}); diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 8571caa49d..5f2d4bf87f 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -16,9 +16,14 @@ import React from 'react'; import { Link } from '@backstage/core'; +import { IndexableDocument } from '@backstage/search-common'; import { ListItem, ListItemText, Divider } from '@material-ui/core'; -export const DefaultResultListItem = ({ result }: any) => { +type Props = { + result: IndexableDocument; +}; + +export const DefaultResultListItem = ({ result }: Props) => { return ( diff --git a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx index c4e3edaa4c..84d3759ad3 100644 --- a/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx +++ b/plugins/search/src/components/SearchResultNext/SearchResultNext.tsx @@ -13,22 +13,19 @@ * 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 React from 'react'; import { useSearch } from '../SearchContext'; -type ChildrenArguments = { - results: SearchResult[]; +type Props = { + children: (results: { results: SearchResult[] }) => JSX.Element; }; -export const SearchResultNext = ({ - children, -}: { - children: (results: ChildrenArguments) => JSX.Element; -}) => { +export const SearchResultNext = ({ children }: Props) => { const { result: { loading, error, value }, } = useSearch(); From 109e186b5f993b85612e9e59cb5bb01b8fd1beba Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 27 May 2021 00:38:48 +0200 Subject: [PATCH 44/56] test(plugins/search): complete AlphaPerformSearchApi coverage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- plugins/search/src/apis.test.ts | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 plugins/search/src/apis.test.ts diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts new file mode 100644 index 0000000000..33714bc688 --- /dev/null +++ b/plugins/search/src/apis.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { CatalogApi } from '@backstage/plugin-catalog-react'; + +import { SearchClient } from './apis'; + +describe('apis', () => { + const query = { + term: '', + filters: {}, + types: [], + pageCursor: '', + }; + + const baseUrl = 'https://base-url.com/'; + const getBaseUrl = jest.fn().mockResolvedValue(baseUrl); + const client = new SearchClient({ + catalogApi: {} as CatalogApi, + discoveryApi: { getBaseUrl }, + }); + + const json = jest.fn(); + const originalFetch = window.fetch; + window.fetch = jest.fn().mockResolvedValue({ json }); + + afterAll(() => { + window.fetch = originalFetch; + }); + + it('Fetch is called with expected URL (including stringified Q params)', async () => { + await client._alphaPerformSearch(query); + expect(getBaseUrl).toHaveBeenLastCalledWith('search/query'); + expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}?term=&pageCursor=`); + }); + + it('Resolves JSON from fetch response', async () => { + const result = { loading: false, error: '', value: {} }; + json.mockReturnValueOnce(result); + expect(await client._alphaPerformSearch(query)).toStrictEqual(result); + }); +}); From 6b8d6ccd13ef9b973bad2567632a86ec0962610e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 27 May 2021 11:47:57 +0200 Subject: [PATCH 45/56] refactor(plugins/search): make components more stylable Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../app/src/components/search/SearchPage.tsx | 133 ++++++++++-------- .../SearchBarNext/SearchBarNext.tsx | 64 ++++----- .../SearchFilterNext/SearchFilterNext.tsx | 28 +++- 3 files changed, 127 insertions(+), 98 deletions(-) diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 3955ec1a2a..221d4a44c4 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -15,67 +15,88 @@ */ import React from 'react'; -import { Content, Header, Lifecycle, Page } from '@backstage/core'; -import { Grid, List, Card, CardContent } from '@material-ui/core'; -import { - SearchBarNext, - SearchResultNext, - DefaultResultListItem, - SearchFilterNext, -} from '@backstage/plugin-search'; -import { CatalogResultListItem } from '@backstage/plugin-catalog'; +import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; -export const searchPage = ( - -
} /> - - - - - - - - - ({ + bar: { + padding: theme.spacing(1, 0), + }, + filters: { + padding: theme.spacing(2), + }, + filter: { + '& + &': { + marginTop: theme.spacing(2.5), + }, + }, +})); + +const SearchPage = () => { + const classes = useStyles(); + + return ( + +
} /> + + + + + + + + + + - - - - - + + + + + {({ results }) => ( + + {results.map(({ type, document }) => { + switch (type) { + case 'software-catalog': + return ( + + ); + default: + return ( + + ); + } + })} + + )} + + - - - {({ results }) => ( - - {results.map(result => { - switch (result.type) { - case 'software-catalog': - return ( - - ); - default: - return ( - - ); - } - })} - - )} - - - - - -); + + + ); +}; + +export const searchPage = ; diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index b223e6c2cd..76bc9bf4f9 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -16,64 +16,52 @@ import React, { ChangeEvent, useState } from 'react'; import { useDebounce } from 'react-use'; -import { - Theme, - Paper, - InputBase, - InputAdornment, - IconButton, - makeStyles, -} from '@material-ui/core'; +import { InputBase, InputAdornment, IconButton } from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; import { useSearch } from '../SearchContext'; -const useStyles = makeStyles((theme: Theme) => ({ - root: { - display: 'flex', - alignItems: 'center', - padding: theme.spacing(0, 0, 0, 1.5), - }, - input: { - flex: 1, - }, -})); - type Props = { + className?: string; debounceTime?: number; }; -export const SearchBarNext = ({ debounceTime = 0 }: Props) => { - const classes = useStyles(); +export const SearchBarNext = ({ className, debounceTime = 0 }: Props) => { const { term, setTerm } = useSearch(); const [value, setValue] = useState(term); useDebounce(() => setTerm(value), debounceTime, [value]); - const handleSearch = (e: ChangeEvent) => { + const handleQuery = (e: ChangeEvent) => { setValue(e.target.value); }; const handleClear = () => setValue(''); return ( - - + + - - } - /> - - - - + + + } + endAdornment={ + + + + + + } + /> ); }; diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx index 0c13fbc008..239707e5ab 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx @@ -35,6 +35,7 @@ const useStyles = makeStyles({ }); export type Component = { + className?: string; name: string; values?: string[]; defaultValue?: string[] | string | null; @@ -45,7 +46,12 @@ export type Props = Component & { debug?: boolean; }; -const CheckboxFilter = ({ name, defaultValue, values = [] }: Component) => { +const CheckboxFilter = ({ + className, + name, + defaultValue, + values = [], +}: Component) => { const classes = useStyles(); const { filters, setFilters } = useSearch(); @@ -72,7 +78,11 @@ const CheckboxFilter = ({ name, defaultValue, values = [] }: Component) => { }; return ( - + {name} {values.map((value: string) => ( { ); }; -const SelectFilter = ({ name, defaultValue, values = [] }: Component) => { +const SelectFilter = ({ + className, + name, + defaultValue, + values = [], +}: Component) => { const classes = useStyles(); const { filters, setFilters } = useSearch(); @@ -120,7 +135,12 @@ const SelectFilter = ({ name, defaultValue, values = [] }: Component) => { }; return ( - + {name} From 5a1c284b2c1fc04a24998bcf823a8d41c39cf121 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 27 May 2021 17:55:06 +0200 Subject: [PATCH 46/56] test(app/components/search): complete Page coverage Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../components/search/SearchPage.js | 121 ++++++++++++++++++ packages/app/cypress/support/commands.js | 19 +++ packages/app/cypress/support/index.js | 1 + 3 files changed, 141 insertions(+) create mode 100644 packages/app/cypress/integration/components/search/SearchPage.js create mode 100644 packages/app/cypress/support/commands.js diff --git a/packages/app/cypress/integration/components/search/SearchPage.js b/packages/app/cypress/integration/components/search/SearchPage.js new file mode 100644 index 0000000000..f97c26ee90 --- /dev/null +++ b/packages/app/cypress/integration/components/search/SearchPage.js @@ -0,0 +1,121 @@ +/* + * 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. + */ + +const API_ENDPOINT = 'http://localhost:7000/api/search/query'; + +describe('SearchPage', () => { + describe('Given a search context with a term, results, and filter values', () => { + it('The results are rendered as expected', () => { + const results = [ + { + type: 'software-catalog', + document: { + title: 'backstage', + text: 'Backstage system documentation', + location: '/result/location/path', + }, + }, + ]; + + cy.enterAsGuest(); + cy.visit('/search-next', { + onBeforeLoad(win) { + cy.stub(win, 'fetch') + .withArgs(`${API_ENDPOINT}?term=&pageCursor=`) + .resolves({ + ok: true, + json: () => ({ results }), + }); + }, + }); + cy.contains('Search'); + + cy.contains(results[0].document.title); + cy.contains(results[0].document.text); + cy.get(`a[href="${results[0].document.location}"]`).should('be.visible'); + }); + + it('The filters are rendered as expected', () => { + cy.enterAsGuest(); + cy.visit( + '/search-next?filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B%5D=experimental', + { + onBeforeLoad(win) { + cy.stub(win, 'fetch') + .withArgs( + `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental&pageCursor=`, + ) + .resolves({ + ok: true, + json: () => ({ results: [] }), + }); + }, + }, + ); + cy.contains('Search'); + + // lifecycle + cy.contains('lifecycle'); + + cy.contains('experimental'); + cy.get( + '[data-testid="search-checkboxfilter-next"] input[value="experimental"]', + ).should('have.attr', 'checked'); + + cy.contains('production'); + cy.get( + '[data-testid="search-checkboxfilter-next"] input[value="production"]', + ).should('not.have.attr', 'checked'); + + // kind + cy.contains('kind'); + cy.get( + '[data-testid="search-selectfilter-next"] [role="button"][aria-haspopup="listbox"]', + ).click(); + + cy.contains('All'); + cy.contains('Template'); + cy.contains('Component'); + + cy.get('[role="option"][data-value="Component"]').should( + 'have.attr', + 'aria-selected', + 'true', + ); + }); + + it('The search bar is rendered as expected', () => { + cy.enterAsGuest(); + cy.visit('/search-next?query=backstage', { + onBeforeLoad(win) { + cy.stub(win, 'fetch') + .withArgs(`${API_ENDPOINT}?term=backstage&pageCursor=`) + .resolves({ + ok: true, + json: () => ({ results: [] }), + }); + }, + }); + cy.contains('Search'); + + cy.get('[data-testid="search-bar-next"] input').should( + 'have.attr', + 'value', + 'backstage', + ); + }); + }); +}); diff --git a/packages/app/cypress/support/commands.js b/packages/app/cypress/support/commands.js new file mode 100644 index 0000000000..dd2b26634d --- /dev/null +++ b/packages/app/cypress/support/commands.js @@ -0,0 +1,19 @@ +/* + * 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. + */ +Cypress.Commands.add('enterAsGuest', () => { + cy.visit('/'); + cy.get('button').contains('Enter').click(); +}); diff --git a/packages/app/cypress/support/index.js b/packages/app/cypress/support/index.js index 8fc8ca91f1..c1f930027a 100644 --- a/packages/app/cypress/support/index.js +++ b/packages/app/cypress/support/index.js @@ -14,3 +14,4 @@ * limitations under the License. */ import '@testing-library/cypress/add-commands'; +import './commands'; From aeb224eb523eeec193e39709e0c2f925587584ef Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 31 May 2021 17:08:52 +0200 Subject: [PATCH 47/56] Resolve yarn build react version conflict something something Signed-off-by: Eric Peterson --- package.json | 1 + yarn.lock | 36 ++++++++---------------------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 1ffeb490a3..7169f0d8e8 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ ] }, "resolutions": { + "**/@types/react": "^16.9.0", "**/@roadiehq/**/@backstage/core": "*", "**/@roadiehq/**/@backstage/plugin-catalog": "*", "**/@roadiehq/**/@backstage/catalog-model": "*", diff --git a/yarn.lock b/yarn.lock index a5832e82da..fa077b9d5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6756,25 +6756,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9": - version "16.9.37" - resolved "https://registry.npmjs.org/@types/react/-/react-16.9.37.tgz#8fb93e7dbd5b1d3796f69aa979a7fe0439bc7bea" - integrity sha512-ZqnAXallQiZ08LTSqMfWMNvAfJEzRLOxdlbbbCIJlYGjU98BEU6bE2uBpKPGeWn+v3hIgCraHKtqUcKZBzMP/Q== - dependencies: - "@types/prop-types" "*" - csstype "^2.2.0" - -"@types/react@16.4.6": - version "16.4.6" - resolved "https://registry.npmjs.org/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada" - integrity sha512-9LDZdhsuKSc+DjY65SjBkA958oBWcTWSVWAd2cD9XqKBjhGw1KzAkRhWRw2eIsXvaIE/TOTjjKMFVC+JA1iU4g== - dependencies: - csstype "^2.2.0" - -"@types/react@>=16.9.0": - version "17.0.8" - resolved "https://registry.npmjs.org/@types/react/-/react-17.0.8.tgz#fe76e3ba0fbb5602704110fd1e3035cf394778e3" - integrity sha512-3sx4c0PbXujrYAKwXxNONXUtRp9C+hE2di0IuxFyf5BELD+B+AXL8G7QrmSKhVwKZDbv0igiAjQAMhXj8Yg3aw== +"@types/react@*", "@types/react@16.4.6", "@types/react@>=16.9.0", "@types/react@^16.9", "@types/react@^16.9.0": + version "16.14.8" + resolved "https://registry.npmjs.org/@types/react/-/react-16.14.8.tgz#4aee3ab004cb98451917c9b7ada3c7d7e52db3fe" + integrity sha512-QN0/Qhmx+l4moe7WJuTxNiTsjBwlBGHqKGvInSQCBdo7Qio0VtOqwsC0Wq7q3PbJlB0cR4Y4CVo1OOe6BOsOmA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -11082,7 +11067,7 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.7: +csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.7: version "2.6.9" resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== @@ -14614,7 +14599,7 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: version "2.8.2" resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== @@ -14624,7 +14609,7 @@ graphql-language-service-interface@^2.8.2: graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: version "1.9.0" resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== @@ -26001,16 +25986,11 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3: +typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== -typescript@~4.1.3: - version "4.1.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== - ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From b9b26ec57277048c896c8cb529957d7013cbd813 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 1 Jun 2021 10:42:42 +0200 Subject: [PATCH 48/56] Update decorator type-specific decorator test Signed-off-by: Eric Peterson --- plugins/search-backend-node/src/IndexBuilder.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index c7a2a7817a..8d28698b18 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -131,7 +131,7 @@ describe('IndexBuilder', () => { // wait for async decorator execution await Promise.resolve(); expect(decoratorSpy).toHaveBeenCalled(); - expect(decoratorSpy).toHaveBeenCalledWith([docFixture]); + expect(decoratorSpy).toHaveBeenCalledWith(expectedType, [docFixture]); }); it('adds a type-specific decorator that should not be called', async () => { From 455b97b127dfc18befd2a6bda053b705ed6666ec Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 1 Jun 2021 11:15:52 +0200 Subject: [PATCH 49/56] Make type(s) readonly properties of Collator/Decorator classes Signed-off-by: Eric Peterson --- packages/backend/src/plugins/search.ts | 1 - packages/search-common/src/types.ts | 7 ++-- .../src/search/DefaultCatalogCollator.ts | 1 + .../src/IndexBuilder.test.ts | 38 +++++++++++-------- .../search-backend-node/src/IndexBuilder.ts | 19 ++++------ plugins/search-backend-node/src/types.ts | 11 ------ 6 files changed, 35 insertions(+), 42 deletions(-) diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 861723ad47..cfd3bb03eb 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -30,7 +30,6 @@ export default async function createPlugin({ const indexBuilder = new IndexBuilder({ logger, searchEngine }); indexBuilder.addCollator({ - type: 'software-catalog', defaultRefreshIntervalSeconds: 10, collator: new DefaultCatalogCollator({ discovery }), }); diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 2aa45e0510..792b9f3a2a 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -58,6 +58,7 @@ export interface IndexableDocument { * search. */ export interface DocumentCollator { + readonly type: string; execute(): Promise; } @@ -66,8 +67,6 @@ export interface DocumentCollator { * additional metadata. */ export interface DocumentDecorator { - execute( - type: string, - documents: IndexableDocument[], - ): Promise; + readonly types?: string[]; + execute(documents: IndexableDocument[]): Promise; } diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 068b9e36ae..70b8010d87 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -30,6 +30,7 @@ export interface CatalogEntityDocument extends IndexableDocument { export class DefaultCatalogCollator implements DocumentCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; + public readonly type: string = 'software-catalog'; constructor({ discovery, diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 8d28698b18..cac2975781 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -24,22 +24,33 @@ import { IndexBuilder } from './IndexBuilder'; import { LunrSearchEngine, SearchEngine } from './index'; class TestDocumentCollator implements DocumentCollator { - async execute() { + readonly type: string = 'anything'; + async execute(): Promise { return []; } } +class TypedDocumentCollator extends TestDocumentCollator { + readonly type = 'an-expected-type'; +} + class TestDocumentDecorator implements DocumentDecorator { - async execute(_type: string, documents: IndexableDocument[]) { + async execute(documents: IndexableDocument[]) { return documents; } } +class TypedDocumentDecorator extends TestDocumentDecorator { + readonly types = ['an-expected-type']; +} + +class DifferentlyTypedDocumentDecorator extends TestDocumentDecorator { + readonly types = ['not-the-expected-type']; +} + describe('IndexBuilder', () => { let testSearchEngine: SearchEngine; let testIndexBuilder: IndexBuilder; - let testCollator: DocumentCollator; - let testDecorator: DocumentDecorator; beforeEach(() => { const logger = getVoidLogger(); @@ -48,18 +59,16 @@ describe('IndexBuilder', () => { logger, searchEngine: testSearchEngine, }); - testCollator = new TestDocumentCollator(); - testDecorator = new TestDocumentDecorator(); }); describe('addCollator', () => { it('adds a collator', async () => { jest.useFakeTimers(); + const testCollator = new TestDocumentCollator(); const collatorSpy = jest.spyOn(testCollator, 'execute'); // Add a collator. testIndexBuilder.addCollator({ - type: 'anything', defaultRefreshIntervalSeconds: 6, collator: testCollator, }); @@ -75,11 +84,12 @@ describe('IndexBuilder', () => { describe('addDecorator', () => { it('adds a decorator', async () => { jest.useFakeTimers(); + const testCollator = new TestDocumentCollator(); + const testDecorator = new TestDocumentDecorator(); const decoratorSpy = jest.spyOn(testDecorator, 'execute'); // Add a collator. testIndexBuilder.addCollator({ - type: 'anything', defaultRefreshIntervalSeconds: 6, collator: testCollator, }); @@ -100,7 +110,8 @@ describe('IndexBuilder', () => { it('adds a type-specific decorator', async () => { jest.useFakeTimers(); - const expectedType = 'an-expected-type'; + const testCollator = new TypedDocumentCollator(); + const testDecorator = new TypedDocumentDecorator(); const docFixture = { title: 'Test', text: 'Test text.', @@ -113,14 +124,12 @@ describe('IndexBuilder', () => { // Add a collator. testIndexBuilder.addCollator({ - type: expectedType, defaultRefreshIntervalSeconds: 6, collator: testCollator, }); // Add a decorator for the same type. testIndexBuilder.addDecorator({ - types: [expectedType], decorator: testDecorator, }); @@ -131,16 +140,17 @@ describe('IndexBuilder', () => { // wait for async decorator execution await Promise.resolve(); expect(decoratorSpy).toHaveBeenCalled(); - expect(decoratorSpy).toHaveBeenCalledWith(expectedType, [docFixture]); + expect(decoratorSpy).toHaveBeenCalledWith([docFixture]); }); it('adds a type-specific decorator that should not be called', async () => { - const expectedType = 'an-expected-type'; const docFixture = { title: 'Test', text: 'Test text.', location: '/test/location', }; + const testCollator = new TestDocumentCollator(); + const testDecorator = new DifferentlyTypedDocumentDecorator(); const collatorSpy = jest .spyOn(testCollator, 'execute') .mockImplementation(async () => [docFixture]); @@ -148,14 +158,12 @@ describe('IndexBuilder', () => { // Add a collator. testIndexBuilder.addCollator({ - type: expectedType, defaultRefreshIntervalSeconds: 6, collator: testCollator, }); // Add a decorator for a different type. testIndexBuilder.addDecorator({ - types: ['not-the-expected-type'], decorator: testDecorator, }); diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index acaa18fc03..046eaf7f34 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -55,28 +55,25 @@ export class IndexBuilder { * given refresh interval. */ addCollator({ - type, collator, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void { this.logger.info( - `Added ${collator.constructor.name} collator for type ${type}`, + `Added ${collator.constructor.name} collator for type ${collator.type}`, ); - this.collators[type] = { + this.collators[collator.type] = { refreshInterval: defaultRefreshIntervalSeconds, collate: collator, }; } /** - * Makes the index builder aware of a decorator. If no types are provided, it - * will be applied to documents from all known collators, otherwise it will - * only be applied to documents of the given types. + * Makes the index builder aware of a decorator. If no types are provided on + * the decorator, it will be applied to documents from all known collators, + * otherwise it will only be applied to documents of the given types. */ - addDecorator({ - types = ['*'], - decorator, - }: RegisterDecoratorParameters): void { + addDecorator({ decorator }: RegisterDecoratorParameters): void { + const types = decorator.types || ['*']; this.logger.info( `Added decorator ${decorator.constructor.name} to types ${types.join( ', ', @@ -113,7 +110,7 @@ export class IndexBuilder { this.logger.debug( `Decorating ${type} documents via ${decorators[i].constructor.name}`, ); - documents = await decorators[i].execute(type, documents); + documents = await decorators[i].execute(documents); } if (!documents || documents.length === 0) { diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index 4abfa77fb1..b47c191a7c 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -26,11 +26,6 @@ import { * Parameters required to register a collator. */ export interface RegisterCollatorParameters { - /** - * The type of document to be indexed (used to name indices, to configure refresh loop, etc). - */ - type: string; - /** * The default interval (in seconds) that the provided collator will be called (can be overridden in config). */ @@ -50,12 +45,6 @@ export interface RegisterDecoratorParameters { * The decorator class responsible for appending or modifying documents of the given type(s). */ decorator: DocumentDecorator; - - /** - * (Optional) An array of document types that the given decorator should apply to. If none are provided, - * the decorator will be applied to all types. - */ - types?: string[]; } /** From 8cb45d747a3e149217161f935ff4987d701154db Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 1 Jun 2021 11:48:04 +0200 Subject: [PATCH 50/56] Make the QueryTranslator a more integral part of the SearchEngine API Signed-off-by: Eric Peterson --- .../src/engines/LunrSearchEngine.test.ts | 49 +++++++++++++++---- .../src/engines/LunrSearchEngine.ts | 8 ++- plugins/search-backend-node/src/types.ts | 13 ++++- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index f597327dbf..969e579a53 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -18,6 +18,15 @@ import { getVoidLogger } from '@backstage/backend-common'; import { LunrSearchEngine } from './LunrSearchEngine'; import { SearchEngine } from '../types'; +/** + * Just used to test the default translator shipped with LunrSearchEngine. + */ +class LunrSearchEngineForTranslatorTests extends LunrSearchEngine { + getTranslator() { + return this.translator; + } +} + describe('LunrSearchEngine', () => { let testLunrSearchEngine: SearchEngine; @@ -27,16 +36,21 @@ describe('LunrSearchEngine', () => { describe('translator', () => { it('query translator invoked', async () => { - const translatorSpy = jest.spyOn(testLunrSearchEngine, 'translator'); + // Given: Set a translator spy on the search engine. + const translatorSpy = jest.fn().mockReturnValue({ + lunrQueryString: '', + documentTypes: [], + }); + testLunrSearchEngine.setTranslator(translatorSpy); - // Translate query and ensure the translator was invoked. - await testLunrSearchEngine.translator({ + // When: querying the search engine + testLunrSearchEngine.query({ term: 'testTerm', filters: {}, pageCursor: '', }); - expect(translatorSpy).toHaveBeenCalled(); + // Then: the translator is invoked with expected args. expect(translatorSpy).toHaveBeenCalledWith({ term: 'testTerm', filters: {}, @@ -45,39 +59,54 @@ describe('LunrSearchEngine', () => { }); it('should return translated query', async () => { - const mockedTranslatedQuery = await testLunrSearchEngine.translator({ + const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + logger: getVoidLogger(), + }); + const translatorUnderTest = inspectableSearchEngine.getTranslator(); + + const actualTranslatedQuery = translatorUnderTest({ term: 'testTerm', filters: {}, pageCursor: '', }); - expect(mockedTranslatedQuery).toMatchObject({ + expect(actualTranslatedQuery).toMatchObject({ documentTypes: ['*'], lunrQueryString: '+testTerm', }); }); it('should return translated query with 1 filter', async () => { - const mockedTranslatedQuery = await testLunrSearchEngine.translator({ + const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + logger: getVoidLogger(), + }); + const translatorUnderTest = inspectableSearchEngine.getTranslator(); + + const actualTranslatedQuery = translatorUnderTest({ term: 'testTerm', filters: { kind: 'testKind' }, pageCursor: '', }); - expect(mockedTranslatedQuery).toMatchObject({ + expect(actualTranslatedQuery).toMatchObject({ documentTypes: ['*'], lunrQueryString: '+testTerm +kind:testKind', }); }); it('should return translated query with multiple filters', async () => { - const mockedTranslatedQuery = await testLunrSearchEngine.translator({ + const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + logger: getVoidLogger(), + }); + const translatorUnderTest = inspectableSearchEngine.getTranslator(); + + const actualTranslatedQuery = translatorUnderTest({ term: 'testTerm', filters: { kind: 'testKind', namespace: 'testNameSpace' }, pageCursor: '', }); - expect(mockedTranslatedQuery).toMatchObject({ + expect(actualTranslatedQuery).toMatchObject({ documentTypes: ['*'], lunrQueryString: '+testTerm +kind:testKind +namespace:testNameSpace', }); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index d6afdc21ae..be51738920 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -33,6 +33,8 @@ type LunrResultEnvelope = { type: string; }; +type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; + export class LunrSearchEngine implements SearchEngine { protected lunrIndices: Record = {}; protected docStore: Record; @@ -43,7 +45,7 @@ export class LunrSearchEngine implements SearchEngine { this.docStore = {}; } - translator: QueryTranslator = ({ + protected translator: QueryTranslator = ({ term, filters, types, @@ -82,6 +84,10 @@ export class LunrSearchEngine implements SearchEngine { }; }; + setTranslator(translator: LunrQueryTranslator) { + this.translator = translator; + } + index(type: string, documents: IndexableDocument[]): void { const lunrBuilder = new lunr.Builder(); // Make this lunr index aware of all relevant fields. diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index b47c191a7c..e7643ee5aa 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -59,7 +59,18 @@ export type QueryTranslator = (query: SearchQuery) => unknown; * concrete, search engine-specific queries. */ export interface SearchEngine { - translator: QueryTranslator; + /** + * Override the default translator provided by the SearchEngine. + */ + setTranslator(translator: QueryTranslator): void; + + /** + * Add the given documents to the SearchEngine index of the given type. + */ index(type: string, documents: IndexableDocument[]): void; + + /** + * Perform a search query against the SearchEngine. + */ query(query: SearchQuery): Promise; } From db1c8f93b31150334af7591a2994cdb4d2a13401 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 1 Jun 2021 12:02:49 +0200 Subject: [PATCH 51/56] Add changesets. Signed-off-by: Eric Peterson --- .changeset/catalog-search-item.md | 5 +++++ .changeset/search-cross-the-goal.md | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/catalog-search-item.md create mode 100644 .changeset/search-cross-the-goal.md diff --git a/.changeset/catalog-search-item.md b/.changeset/catalog-search-item.md new file mode 100644 index 0000000000..c1a12ec161 --- /dev/null +++ b/.changeset/catalog-search-item.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +A `` component is now available for use in custom Search Experiences. diff --git a/.changeset/search-cross-the-goal.md b/.changeset/search-cross-the-goal.md new file mode 100644 index 0000000000..580b28ac23 --- /dev/null +++ b/.changeset/search-cross-the-goal.md @@ -0,0 +1,9 @@ +--- +'@backstage/search-common': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-search': patch +--- + +The ` set of components exported by the Search Plugin are now updated to use the Search Backend API. These will be made available as the default non-"next" versions in a follow-up release. + +The interfaces for decorators and collators in the Search Backend have also seen minor, breaking revisions ahead of a general release. If you happen to be building on top of these interfaces, check and update your implementations accordingly. The APIs will be considered more stable in a follow-up release. From d0d2bf79c8253630d5fed8327cb208e084afb636 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 1 Jun 2021 12:13:36 +0200 Subject: [PATCH 52/56] API Report and document types. Signed-off-by: Eric Peterson --- packages/search-common/api-report.md | 4 +++- packages/search-common/src/types.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index 35a868849d..a3de27b5a9 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -10,12 +10,14 @@ import { JsonObject } from '@backstage/config'; export interface DocumentCollator { // (undocumented) execute(): Promise; + readonly type: string; } // @public export interface DocumentDecorator { // (undocumented) - execute(type: string, documents: IndexableDocument[]): Promise; + execute(documents: IndexableDocument[]): Promise; + readonly types?: string[]; } // @public diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 792b9f3a2a..2e7ca97edc 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -58,6 +58,10 @@ export interface IndexableDocument { * search. */ export interface DocumentCollator { + /** + * The type or name of the document set returned by this collator. Used as an + * index name by Search Engines. + */ readonly type: string; execute(): Promise; } @@ -67,6 +71,11 @@ export interface DocumentCollator { * additional metadata. */ export interface DocumentDecorator { + /** + * An optional array of document/index types on which this decorator should + * be applied. If no types are provided, this decorator will be applied to + * all document/index types. + */ readonly types?: string[]; execute(documents: IndexableDocument[]): Promise; } From 311ca9f517c795041afc8b18132dc86c61cb4a57 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 2 Jun 2021 08:27:48 +0200 Subject: [PATCH 53/56] fix: change copyright year to 2021 Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- .../app/cypress/integration/components/search/SearchPage.js | 2 +- plugins/search/src/apis.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/cypress/integration/components/search/SearchPage.js b/packages/app/cypress/integration/components/search/SearchPage.js index f97c26ee90..4e13fc8a0f 100644 --- a/packages/app/cypress/integration/components/search/SearchPage.js +++ b/packages/app/cypress/integration/components/search/SearchPage.js @@ -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. diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 33714bc688..32ec0f0d0e 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -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. From 9f31301bc8c4e6dfb14469126271ea2c02f456fd Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 2 Jun 2021 08:35:47 +0200 Subject: [PATCH 54/56] fix: reset Lunr refresh interval Signed-off-by: Camila Belo Signed-off-by: Eric Peterson --- packages/backend/src/plugins/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index cfd3bb03eb..6688b9c158 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -30,7 +30,7 @@ export default async function createPlugin({ const indexBuilder = new IndexBuilder({ logger, searchEngine }); indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 10, + defaultRefreshIntervalSeconds: 600, collator: new DefaultCatalogCollator({ discovery }), }); From e71458c208964509b1351b4bbc895522c88db224 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 4 Jun 2021 12:30:56 +0200 Subject: [PATCH 55/56] Standard @testing-library/react-hooks version Signed-off-by: Eric Peterson --- package.json | 1 - plugins/search/package.json | 2 +- yarn.lock | 62 +++++++++++++------------------------ 3 files changed, 22 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 7169f0d8e8..1ffeb490a3 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ ] }, "resolutions": { - "**/@types/react": "^16.9.0", "**/@roadiehq/**/@backstage/core": "*", "**/@roadiehq/**/@backstage/plugin-catalog": "*", "**/@roadiehq/**/@backstage/catalog-model": "*", diff --git a/plugins/search/package.json b/plugins/search/package.json index f2c10b89c4..a74091e497 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -52,7 +52,7 @@ "@backstage/test-utils": "^0.1.13", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/react-hooks": "^7.0.0", + "@testing-library/react-hooks": "^3.4.2", "@testing-library/user-event": "^13.1.8", "@types/react": "^16.9", "@types/jest": "^26.0.7", diff --git a/yarn.lock b/yarn.lock index fa077b9d5f..799631f8bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1702,7 +1702,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.7.4": - version "0.8.0" + version "0.8.1" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1716,7 +1716,7 @@ yup "^0.29.3" "@backstage/catalog-model@^0.7.9": - version "0.8.0" + version "0.8.1" dependencies: "@backstage/config" "^0.1.5" "@backstage/errors" "^0.1.1" @@ -1746,16 +1746,16 @@ react-use "^17.2.4" "@backstage/plugin-catalog@^0.5.1": - version "0.6.0" + version "0.6.1" dependencies: "@backstage/catalog-client" "^0.3.12" - "@backstage/catalog-model" "^0.8.0" - "@backstage/core" "^0.7.11" + "@backstage/catalog-model" "^0.8.1" + "@backstage/core" "^0.7.12" "@backstage/errors" "^0.1.1" - "@backstage/integration" "^0.5.4" + "@backstage/integration" "^0.5.5" "@backstage/integration-react" "^0.1.2" - "@backstage/plugin-catalog-react" "^0.2.0" - "@backstage/theme" "^0.2.7" + "@backstage/plugin-catalog-react" "^0.2.1" + "@backstage/theme" "^0.2.8" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" @@ -5700,17 +5700,6 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.4.0" -"@testing-library/react-hooks@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.0.tgz#dd6d37a7e018f147a3b9153137f10e013be8472b" - integrity sha512-WFBGH8DWdIGGBHt6PBtQPe2v4Kbj9vQ1sQ9qLBTmwn1PNggngint4MTE/IiWCYhPbyTW3oc/7X62DObMn/AjQQ== - dependencies: - "@babel/runtime" "^7.12.5" - "@types/react" ">=16.9.0" - "@types/react-dom" ">=16.9.0" - "@types/react-test-renderer" ">=16.9.0" - react-error-boundary "^3.1.0" - "@testing-library/react@^11.2.5": version "11.2.6" resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" @@ -6678,13 +6667,6 @@ "@types/webpack" "^4" "@types/webpack-dev-server" "*" -"@types/react-dom@>=16.9.0": - version "17.0.5" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227" - integrity sha512-ikqukEhH4H9gr4iJCmQVNzTB307kROe3XFfHAOTxOXPOw7lAoEXnM5KWTkzeANGL5Ce6ABfiMl/zJBYNi7ObmQ== - dependencies: - "@types/react" "*" - "@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" @@ -6728,13 +6710,6 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@>=16.9.0": - version "17.0.1" - resolved "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" - integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== - dependencies: - "@types/react" "*" - "@types/react-text-truncate@^0.14.0": version "0.14.0" resolved "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.0.tgz#588bbabbc7f2a13815e805f3a48942db73fe65fe" @@ -6756,7 +6731,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@16.4.6", "@types/react@>=16.9.0", "@types/react@^16.9", "@types/react@^16.9.0": +"@types/react@*", "@types/react@^16.9": version "16.14.8" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.8.tgz#4aee3ab004cb98451917c9b7ada3c7d7e52db3fe" integrity sha512-QN0/Qhmx+l4moe7WJuTxNiTsjBwlBGHqKGvInSQCBdo7Qio0VtOqwsC0Wq7q3PbJlB0cR4Y4CVo1OOe6BOsOmA== @@ -6765,6 +6740,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@16.4.6": + version "16.4.6" + resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada" + integrity sha1-UCSVfGvO9PAoI6zPWXT6ui5U+to= + dependencies: + csstype "^2.2.0" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -11067,6 +11049,11 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +csstype@^2.2.0: + version "2.6.17" + resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e" + integrity sha1-TPMOuH4dGgBdi2UQ+VKSQT9qHA4= + csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.7: version "2.6.9" resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" @@ -22206,13 +22193,6 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" -react-error-boundary@^3.1.0: - version "3.1.3" - resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz#276bfa05de8ac17b863587c9e0647522c25e2a0b" - integrity sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA== - dependencies: - "@babel/runtime" "^7.12.5" - react-error-overlay@^6.0.7, react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" From abd53e76a0a3337f66bfda95c3f52bc7e846fc90 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 4 Jun 2021 14:34:32 +0200 Subject: [PATCH 56/56] Only set default filter values on initial mount. Signed-off-by: Eric Peterson --- .../src/components/SearchFilterNext/SearchFilterNext.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx index 239707e5ab..7beef58bc9 100644 --- a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx @@ -62,7 +62,8 @@ const CheckboxFilter = ({ [name]: defaultValue, })); } - }, [name, defaultValue, setFilters]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const handleChange = (e: ChangeEvent) => { const { @@ -121,7 +122,8 @@ const SelectFilter = ({ [name]: defaultValue, })); } - }, [name, defaultValue, setFilters]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const handleChange = (e: ChangeEvent<{ value: unknown }>) => { const {