composability refactoring
Signed-off-by: Emma Indal <emma.indahl@gmail.com> Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
committed by
Eric Peterson
parent
1dbc719249
commit
c8284149c2
@@ -16,31 +16,15 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Content, Header, Lifecycle, Page } from '@backstage/core';
|
||||
import { Grid, List } from '@material-ui/core';
|
||||
import { Grid, List, Card, CardContent } from '@material-ui/core';
|
||||
import {
|
||||
SearchBarNext,
|
||||
SearchResultNext,
|
||||
DefaultResultListItem,
|
||||
SearchFiltersNext,
|
||||
FilterType,
|
||||
CheckBoxFilter,
|
||||
SelectFilter,
|
||||
SearchFilterNext,
|
||||
} from '@backstage/plugin-search';
|
||||
import { CatalogResultListItem } from '@backstage/plugin-catalog';
|
||||
|
||||
const filterDefinitions = [
|
||||
{
|
||||
field: 'kind',
|
||||
type: FilterType.SELECT,
|
||||
values: ['Component', 'Template'],
|
||||
},
|
||||
{
|
||||
field: 'lifecycle',
|
||||
type: FilterType.CHECKBOX,
|
||||
values: ['experimental', 'production'],
|
||||
},
|
||||
];
|
||||
|
||||
export const searchPage = (
|
||||
<Page themeId="home">
|
||||
<Header title="Search" subtitle={<Lifecycle alpha />} />
|
||||
@@ -50,32 +34,20 @@ export const searchPage = (
|
||||
<SearchBarNext />
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<SearchFiltersNext>
|
||||
<>
|
||||
{filterDefinitions.map(definition => {
|
||||
switch (definition.type) {
|
||||
case 'checkbox':
|
||||
return (
|
||||
<CheckBoxFilter
|
||||
key={definition.field}
|
||||
fieldName={definition.field}
|
||||
values={definition.values}
|
||||
/>
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<SelectFilter
|
||||
key={definition.field}
|
||||
fieldName={definition.field}
|
||||
values={definition.values}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</>
|
||||
</SearchFiltersNext>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<SearchFilterNext.Select
|
||||
name="kind"
|
||||
values={['Component', 'Template']}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<SearchFilterNext.Checkbox
|
||||
name="lifecycle"
|
||||
values={['experimental', 'production']}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<SearchResultNext>
|
||||
|
||||
@@ -36,11 +36,7 @@ export const SearchBarNext = () => {
|
||||
const classes = useStyles();
|
||||
const { term, setTerm, setPageCursor } = useSearch();
|
||||
|
||||
const [queryString, setQueryString] = useQueryParamState<string>('query');
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(queryString ?? '');
|
||||
}, [queryString, setTerm]);
|
||||
const [, setQueryString] = useQueryParamState<string>('query');
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
|
||||
@@ -39,6 +39,11 @@ type SearchContextValue = {
|
||||
setPageCursor: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
type SettableSearchContext = Omit<
|
||||
SearchContextValue,
|
||||
'result' | 'setTerm' | 'setTypes' | 'setFilters' | 'setPageCursor'
|
||||
>;
|
||||
|
||||
const SearchContext = createContext({} as SearchContextValue);
|
||||
|
||||
export const SearchContextProvider = ({
|
||||
@@ -49,7 +54,7 @@ export const SearchContextProvider = ({
|
||||
types: ['*'],
|
||||
},
|
||||
children,
|
||||
}: PropsWithChildren<{ initialState?: any }>) => {
|
||||
}: PropsWithChildren<{ initialState?: SettableSearchContext }>) => {
|
||||
const searchApi = useApi(searchApiRef);
|
||||
const [pageCursor, setPageCursor] = useState<string>(initialState.pageCursor);
|
||||
const [filters, setFilters] = useState<JsonObject>(initialState.filters);
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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, { ReactElement, useEffect } from 'react';
|
||||
import {
|
||||
makeStyles,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
InputLabel,
|
||||
Checkbox,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormLabel,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { useSearch } from '../SearchContext';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
select: {
|
||||
width: '100%',
|
||||
},
|
||||
subtitle: {
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
});
|
||||
|
||||
export type Component = Omit<Props, 'component' | 'debug'>;
|
||||
|
||||
export type Props = {
|
||||
component: (props: Component) => ReactElement;
|
||||
debug?: boolean;
|
||||
name: string;
|
||||
values?: string[];
|
||||
defaultValue?: 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();
|
||||
|
||||
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) {
|
||||
setFilters(prevFilters => ({
|
||||
...prevFilters,
|
||||
[name]: defaultValue.split(','),
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValue, setFilters]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel>{name}</FormLabel>
|
||||
{values &&
|
||||
values.map((v: string) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
color="primary"
|
||||
tabIndex={-1}
|
||||
inputProps={{ 'aria-labelledby': v }}
|
||||
value={v}
|
||||
name={v}
|
||||
onChange={() => setCheckboxFilter(v)}
|
||||
checked={
|
||||
filters[name]
|
||||
? (filters[name] as string[]).includes(v)
|
||||
: false
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={v}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
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) {
|
||||
setFilters(prevFilters => ({
|
||||
...prevFilters,
|
||||
[name]: defaultValue,
|
||||
}));
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[setFilters, defaultValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl variant="filled" className={classes.select}>
|
||||
<InputLabel margin="dense">{name}</InputLabel>
|
||||
<Select
|
||||
variant="outlined"
|
||||
value={filters[name] || ''}
|
||||
onChange={(e: React.ChangeEvent<any>) =>
|
||||
setSelectFilter(e?.target?.value)
|
||||
}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>All</em>
|
||||
</MenuItem>
|
||||
{values &&
|
||||
values.map((value: string) => (
|
||||
<MenuItem value={value}>{value}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchFilterNext = ({ component: Element, ...props }: Props) => {
|
||||
return <Element {...props} />;
|
||||
};
|
||||
|
||||
SearchFilterNext.Checkbox = (props: Omit<Props, 'component'>) => (
|
||||
<SearchFilterNext {...props} component={CheckboxFilter} />
|
||||
);
|
||||
SearchFilterNext.Select = (props: Omit<Props, 'component'>) => (
|
||||
<SearchFilterNext {...props} component={SelectFilter} />
|
||||
);
|
||||
|
||||
export { SearchFilterNext };
|
||||
@@ -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';
|
||||
@@ -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<string>('query');
|
||||
const filters = (qs.parse(location.search.substring(1), { arrayLimit: 0 })
|
||||
.filters || {}) as JsonObject;
|
||||
const initialState = {
|
||||
term: queryString || '',
|
||||
types: [],
|
||||
pageCursor: '',
|
||||
filters,
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchContextProvider>
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<Outlet />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
export * from './Filters';
|
||||
export * from './SearchFilterNext';
|
||||
export * from './SearchFiltersNext';
|
||||
export * from './SearchBar';
|
||||
export * from './SearchBarNext';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -100,12 +100,3 @@ export const DefaultResultListItem = searchPlugin.provide(
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const SearchFiltersNext = searchPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/SearchFiltersNext').then(m => m.SearchFiltersNext),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user