entityTypeFilter improvements

- Supports selecting multiple types
- Switch TemplateTypePicker to FormGroup instead of List

Co-authored-by: Joe Porpeglia <josephp@spotify.com>
Signed-off-by: Tim Hansen <timbonicus@gmail.com>
This commit is contained in:
Tim Hansen
2021-06-28 16:04:43 -06:00
parent e5abca34f6
commit 315deb8544
6 changed files with 84 additions and 79 deletions
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { capitalize } from 'lodash';
import capitalize from 'lodash/capitalize';
import { Box } from '@material-ui/core';
import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter';
@@ -24,9 +24,14 @@ import { Select } from '@backstage/core-components';
export const EntityTypePicker = () => {
const alertApi = useApi(alertApiRef);
const { error, types, selectedType, setType } = useEntityTypeFilter();
const {
error,
availableTypes,
selectedTypes,
setSelectedTypes,
} = useEntityTypeFilter();
if (!types) return null;
if (!availableTypes) return null;
if (error) {
alertApi.post({
@@ -38,7 +43,7 @@ export const EntityTypePicker = () => {
const items = [
{ value: 'all', label: 'All' },
...types.map((type: string) => ({
...availableTypes.map((type: string) => ({
value: type,
label: capitalize(type),
})),
@@ -49,8 +54,10 @@ export const EntityTypePicker = () => {
<Select
label="Type"
items={items}
selected={selectedType ?? 'all'}
onChange={value => setType(value === 'all' ? undefined : String(value))}
selected={selectedTypes.length ? selectedTypes[0] : 'all'}
onChange={value =>
setSelectedTypes(value === 'all' ? [] : [String(value)])
}
/>
</Box>
);
+2 -1
View File
@@ -34,7 +34,8 @@ export class EntityKindFilter implements EntityFilter {
export class EntityTypeFilter implements EntityFilter {
constructor(readonly value: string | string[]) {}
getTypes() {
// Simplify `string | string[]` for consumers, always returns an array
getTypes(): string[] {
return Array.isArray(this.value) ? this.value : [this.value];
}
@@ -178,7 +178,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
};
export function useEntityListProvider<
EntityFilters extends DefaultEntityFilters
EntityFilters extends DefaultEntityFilters = DefaultEntityFilters
>(): EntityListContextProps<EntityFilters> {
const context = useContext(EntityListContext);
if (!context)
@@ -27,10 +27,9 @@ import { EntityTypeFilter } from '../filters';
type EntityTypeReturn = {
loading: boolean;
error?: Error;
types: string[];
selectedType: string | undefined;
setType: (type: string | undefined) => void;
setTypes: (types: string[]) => void;
availableTypes: string[];
selectedTypes: string[];
setSelectedTypes: (types: string[]) => void;
};
/**
@@ -44,7 +43,7 @@ export function useEntityTypeFilter(): EntityTypeReturn {
updateFilters,
} = useEntityListProvider();
const [allTypes, setAllTypes] = useState<string[]>([]);
const [availableTypes, setAvailableTypes] = useState<string[]>([]);
const kind = useMemo(() => kindFilter?.value, [kindFilter]);
// Load all valid spec.type values straight from the catalogApi, paying attention to only the
@@ -65,12 +64,23 @@ export function useEntityTypeFilter(): EntityTypeReturn {
useEffect(() => {
// Resolve the unique set of types from returned entities; could be optimized by a new endpoint
// in the catalog-backend that does this, rather than loading entities with redundant types.
const newTypes = [
...new Set(
(entities ?? []).map(e => e.spec?.type).filter(Boolean) as string[],
),
].sort();
setAllTypes(newTypes);
if (!entities) return;
// Sort by entity count descending, so the most common types appear on top
const countByType = entities.reduce((acc, entity) => {
if (typeof entity.spec?.type !== 'string') return acc;
if (!acc[entity.spec.type]) {
acc[entity.spec.type] = 0;
}
acc[entity.spec.type] += 1;
return acc;
}, {} as Record<string, number>);
const newTypes = Object.entries(countByType)
.sort(([, count1], [, count2]) => count2 - count1)
.map(([type]) => type);
setAvailableTypes(newTypes);
// Update type filter to only valid values when the list of available types has changed
updateFilters((oldFilters: DefaultEntityFilters) => {
@@ -89,25 +99,19 @@ export function useEntityTypeFilter(): EntityTypeReturn {
});
}, [updateFilters, entities]);
const setTypes = useCallback(
const setSelectedTypes = useCallback(
(types: string[]) =>
updateFilters({
type: types.length ? undefined : new EntityTypeFilter(types),
type: types.length ? new EntityTypeFilter(types) : undefined,
}),
[updateFilters],
);
const setType = (type: string | undefined) =>
setTypes(type === undefined ? [] : [type]);
// TODO(timbonicus): selectedType should be selectedTypes
// TODO(timbonicus): remove setType, make this only array-based
return {
loading,
error,
types: allTypes,
selectedType: typeFilter?.value,
setType,
setTypes,
availableTypes,
selectedTypes: typeFilter?.getTypes() ?? [],
setSelectedTypes,
};
}
@@ -60,7 +60,7 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => {
const { loading, error, entities, filters } = useEntityListProvider();
const showTypeColumn = filters.type === undefined;
// TODO(timbonicus): we should show filter chips for all filters instead
// TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar
const titlePreamble = capitalize(filters.user?.value ?? 'all');
if (error) {
@@ -15,31 +15,40 @@
*/
import React from 'react';
import capitalize from 'lodash/capitalize';
import { Progress } from '@backstage/core-components';
import {
Typography,
List,
ListItem,
Box,
Checkbox,
FormControlLabel,
FormGroup,
makeStyles,
Theme,
Checkbox,
ListItemText,
Typography,
} from '@material-ui/core';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
const useStyles = makeStyles<Theme>(theme => ({
checkbox: {
padding: theme.spacing(0, 1, 0, 1),
padding: theme.spacing(1, 1, 1, 2),
},
}));
export const TemplateTypePicker = () => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
// TODO(timbonicus): Use new setTypes returned from the hook
const { error, types, selectedType } = useEntityTypeFilter();
const {
error,
loading,
availableTypes,
selectedTypes,
setSelectedTypes,
} = useEntityTypeFilter();
if (!types) return null;
if (loading) return <Progress />;
if (!availableTypes) return null;
if (error) {
alertApi.post({
@@ -49,49 +58,33 @@ export const TemplateTypePicker = () => {
return null;
}
function toggleSelection(type: string) {
setSelectedTypes(
selectedTypes.includes(type)
? selectedTypes.filter(t => t !== type)
: [...selectedTypes, type],
);
}
return (
<>
<Box pb={1} pt={1}>
<Typography variant="button">Categories</Typography>
<List disablePadding dense>
{types.map(type => {
const labelId = `checkbox-list-label-${type}`;
return (
<ListItem
key={type}
dense
button
onClick={() => {}}
// TODO(timbonicus): Update to use setTypes
// setSelectedCategories(
// selectedCategories.includes(type)
// ? selectedCategories.filter(
// selectedCategory => selectedCategory !== type,
// )
// : [...selectedCategories, type],
// )
// }
>
<FormGroup>
{availableTypes.map(type => (
<FormControlLabel
control={
<Checkbox
edge="start"
color="primary"
// TODO: Fix me
// checked={selectedTypes.includes(type)}
checked={type === selectedType}
tabIndex={-1}
disableRipple
checked={selectedTypes.includes(type)}
onChange={() => toggleSelection(type)}
name={`entity-type-option-${type}`}
className={classes.checkbox}
inputProps={{ 'aria-labelledby': labelId }}
/>
<ListItemText
id={labelId}
primary={
type.charAt(0).toLocaleUpperCase('en-US') + type.slice(1)
}
/>
</ListItem>
);
})}
</List>
</>
}
label={capitalize(type)}
key={type}
/>
))}
</FormGroup>
</Box>
);
};