composability refactoring

Signed-off-by: Emma Indal <emma.indahl@gmail.com>
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Emma Indal
2021-05-24 12:35:07 +02:00
committed by Eric Peterson
parent 1dbc719249
commit c8284149c2
9 changed files with 234 additions and 62 deletions
@@ -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>
);
+1
View File
@@ -15,6 +15,7 @@
*/
export * from './Filters';
export * from './SearchFilterNext';
export * from './SearchFiltersNext';
export * from './SearchBar';
export * from './SearchBarNext';
+1 -1
View File
@@ -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';
-9
View File
@@ -100,12 +100,3 @@ export const DefaultResultListItem = searchPlugin.provide(
},
}),
);
export const SearchFiltersNext = searchPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/SearchFiltersNext').then(m => m.SearchFiltersNext),
},
}),
);