Scaffolder: Replace Tag Filter with Category Filter

This commit is contained in:
Oscar Hernandez
2021-02-08 16:56:11 -06:00
parent bff8620448
commit 44432a08b5
9 changed files with 65 additions and 90 deletions
@@ -50,28 +50,28 @@ const useStyles = makeStyles<Theme>(theme => ({
}));
type Props = {
availableTags: string[];
availableCategories: string[];
};
/**
* The additional results filter in the sidebar.
*/
export const ResultsFilter = ({ availableTags }: Props) => {
export const ResultsFilter = ({ availableCategories }: Props) => {
const classes = useStyles();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const context = useContext(filterGroupsContext);
if (!context) {
throw new Error(`Must be used inside an EntityFilterGroupsProvider`);
}
const setSelectedTagsFilter = context?.setSelectedTags;
const setSelectedCatgoriesFilter = context?.setSelectedCategories;
const updateSelectedTags = useCallback(
(tags: string[]) => {
setSelectedTags(tags);
setSelectedTagsFilter(tags);
const updateSelectedCategories = useCallback(
(categories: string[]) => {
setSelectedCategories(categories);
setSelectedCatgoriesFilter(categories);
},
[setSelectedTags, setSelectedTagsFilter],
[setSelectedCategories, setSelectedCatgoriesFilter],
);
return (
@@ -80,14 +80,14 @@ export const ResultsFilter = ({ availableTags }: Props) => {
<Typography variant="subtitle2" className={classes.filterBoxTitle}>
Refine Results
</Typography>{' '}
<Button onClick={() => updateSelectedTags([])}>Clear</Button>
<Button onClick={() => updateSelectedCategories([])}>Clear</Button>
</div>
<Divider />
<Typography variant="subtitle2" className={classes.title}>
Tags
Categories
</Typography>
<List disablePadding dense>
{availableTags.map(t => {
{availableCategories.map(t => {
const labelId = `checkbox-list-label-${t}`;
return (
<ListItem
@@ -95,23 +95,26 @@ export const ResultsFilter = ({ availableTags }: Props) => {
dense
button
onClick={() =>
updateSelectedTags(
selectedTags.includes(t)
? selectedTags.filter(s => s !== t)
: [...selectedTags, t],
updateSelectedCategories(
selectedCategories.includes(t)
? selectedCategories.filter(s => s !== t)
: [...selectedCategories, t],
)
}
>
<Checkbox
edge="start"
color="primary"
checked={selectedTags.includes(t)}
checked={selectedCategories.includes(t)}
tabIndex={-1}
disableRipple
className={classes.checkbox}
inputProps={{ 'aria-labelledby': labelId }}
/>
<ListItemText id={labelId} primary={t} />
<ListItemText
id={labelId}
primary={t.charAt(0).toUpperCase() + t.slice(1)}
/>
</ListItem>
);
})}
@@ -28,7 +28,7 @@ import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { EntityFilterGroupsProvider } from '../../filter';
import { ButtonGroup, CatalogFilter } from './CatalogFilter';
import { ButtonGroup, ScaffolderFilter } from './ScaffolderFilter';
describe('Catalog Filter', () => {
const catalogApi: Partial<CatalogApi> = {
@@ -86,7 +86,7 @@ describe('Catalog Filter', () => {
{ name: 'Test Group 2', items: [] },
];
const { findByText } = renderWrapped(
<CatalogFilter buttonGroups={mockGroups} initiallySelected="" />,
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="" />,
);
for (const group of mockGroups) {
expect(await findByText(group.name)).toBeInTheDocument();
@@ -113,7 +113,7 @@ describe('Catalog Filter', () => {
];
const { findByText } = renderWrapped(
<CatalogFilter buttonGroups={mockGroups} initiallySelected="all" />,
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="all" />,
);
for (const item of mockGroups[0].items) {
@@ -143,7 +143,7 @@ describe('Catalog Filter', () => {
const onChange = jest.fn();
renderWrapped(
<CatalogFilter
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="all"
onChange={onChange}
@@ -180,7 +180,7 @@ describe('Catalog Filter', () => {
const onChange = jest.fn();
renderWrapped(
<CatalogFilter
<ScaffolderFilter
buttonGroups={mockGroups}
onChange={onChange}
initiallySelected="starred"
@@ -217,7 +217,7 @@ describe('Catalog Filter', () => {
const onChange = jest.fn();
const { findByText } = renderWrapped(
<CatalogFilter
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="all"
onChange={onChange}
@@ -256,7 +256,7 @@ describe('Catalog Filter', () => {
];
const { findByText } = renderWrapped(
<CatalogFilter buttonGroups={mockGroups} initiallySelected="owned" />,
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="owned" />,
);
expect(await findByText('1')).toBeInTheDocument();
@@ -83,7 +83,7 @@ type Props = {
/**
* The main filter group in the sidebar, toggling owned/starred/all.
*/
export const CatalogFilter = ({
export const ScaffolderFilter = ({
buttonGroups,
onChange,
initiallySelected,
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { CatalogFilter } from './CatalogFilter';
export { ScaffolderFilter } from './ScaffolderFilter';
@@ -19,7 +19,6 @@ import {
configApiRef,
Content,
ContentHeader,
errorApiRef,
Header,
Lifecycle,
Page,
@@ -28,7 +27,7 @@ import {
useApi,
WarningPanel,
} from '@backstage/core';
import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react';
import { isOwnerOf } from '@backstage/plugin-catalog-react';
import {
Button,
FormControl,
@@ -36,27 +35,18 @@ import {
IconButton,
Input,
InputAdornment,
InputLabel,
Link,
makeStyles,
TextField,
Toolbar,
Typography,
} from '@material-ui/core';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import useStaleWhileRevalidate from 'swr';
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { TemplateCard, TemplateCardProps } from '../TemplateCard';
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
import { CatalogFilter } from '../CatalogFilter';
import { ButtonGroup } from '../CatalogFilter/CatalogFilter';
import { ScaffolderFilter } from '../ScaffolderFilter';
import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter';
import SettingsIcon from '@material-ui/icons/Settings';
import StarIcon from '@material-ui/icons/Star';
import { useOwnUser } from '../useOwnUser';
@@ -96,29 +86,14 @@ export const ScaffolderPageContents = () => {
const {
loading,
error,
reload,
reload, // TODO: Configure reload
matchingEntities,
availableTags, // TODO: Change tags to Categories
availableCategories, // TODO: Change tags to Categories
isCatalogEmpty,
} = useFilteredEntities();
// TODO: use Selected Sidebar Item
const [selectedSidebarItem, setSelectedSidebarItem] = useState<string>();
const catalogApi = useApi(catalogApiRef);
const errorApi = useApi(errorApiRef);
const {
data: templates /* isValidating*/ /* , error */,
} = useStaleWhileRevalidate('templates/all', async () => {
const response = await catalogApi.getEntities({
filter: { kind: 'Template' },
});
return response.items as TemplateEntityV1alpha1[];
});
useEffect(() => {
if (!error) return;
errorApi.post(error);
}, [error, errorApi]);
const { value: user } = useOwnUser();
const configApi = useApi(configApiRef);
@@ -237,12 +212,12 @@ export const ScaffolderPageContents = () => {
</FormControl>
</Toolbar>
<CatalogFilter
<ScaffolderFilter
buttonGroups={filterGroups} // TODO: filterGroups??
onChange={({ label }) => setSelectedSidebarItem(label)} // TODO: setSelecteSidebarItem
initiallySelected="all"
/>
<ResultsFilter availableTags={availableTags} />
<ResultsFilter availableCategories={availableCategories} />
</div>
<div>
{!filteredTemplates && loading && <Progress />}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Entity, TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { useApi } from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
@@ -59,12 +59,12 @@ function useProvideEntityFilters(): FilterGroupsContext {
const selectedFilterKeys = useRef<{
[filterGroupId: string]: Set<string>;
}>({});
const selectedTags = useRef<string[]>([]);
const selectedCategories = useRef<string[]>([]);
const [filterGroupStates, setFilterGroupStates] = useState<{
[filterGroupId: string]: FilterGroupStates;
}>({});
const [matchingEntities, setMatchingEntities] = useState<Entity[]>([]);
const [availableTags, setAvailableTags] = useState<string[]>([]);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
const [isCatalogEmpty, setCatalogEmpty] = useState<boolean>(false);
useEffect(() => {
@@ -76,7 +76,7 @@ function useProvideEntityFilters(): FilterGroupsContext {
buildStates(
filterGroups.current,
selectedFilterKeys.current,
selectedTags.current,
selectedCategories.current,
entities,
error,
),
@@ -85,11 +85,11 @@ function useProvideEntityFilters(): FilterGroupsContext {
buildMatchingEntities(
filterGroups.current,
selectedFilterKeys.current,
selectedTags.current,
selectedCategories.current,
entities,
),
);
setAvailableTags(collectTags(entities));
setAvailableCategories(collectCategories(entities));
setCatalogEmpty(entities !== undefined && entities.length === 0);
}, [entities, error]);
@@ -125,9 +125,9 @@ function useProvideEntityFilters(): FilterGroupsContext {
[rebuild],
);
const setSelectedTags = useCallback(
(tags: string[]) => {
selectedTags.current = tags;
const setSelectedCategories = useCallback(
(categories: string[]) => {
selectedCategories.current = categories;
rebuild();
},
[rebuild],
@@ -141,13 +141,13 @@ function useProvideEntityFilters(): FilterGroupsContext {
register,
unregister,
setGroupSelectedFilters,
setSelectedTags,
setSelectedCategories,
reload,
loading: !error && !entities,
error,
filterGroupStates,
matchingEntities,
availableTags,
availableCategories,
isCatalogEmpty,
};
}
@@ -157,7 +157,7 @@ function useProvideEntityFilters(): FilterGroupsContext {
function buildStates(
filterGroups: { [filterGroupId: string]: FilterGroup },
selectedFilterKeys: { [filterGroupId: string]: Set<string> },
selectedTags: string[],
selectedCategories: string[],
entities?: Entity[],
error?: Error,
): { [filterGroupId: string]: FilterGroupStates } {
@@ -186,7 +186,7 @@ function buildStates(
const otherMatchingEntities = buildMatchingEntities(
filterGroups,
selectedFilterKeys,
selectedTags,
selectedCategories,
entities,
filterGroupId,
);
@@ -204,15 +204,15 @@ function buildStates(
return result;
}
// Given all entites, find all possible tags and provide them in a sorted list.
function collectTags(entities?: Entity[]): string[] {
const tags = new Set<string>();
// Given all entites, find all possible categories and provide them in a sorted list.
function collectCategories(entities?: Entity[]): string[] {
const categories = new Set<string>();
(entities || []).forEach(e => {
if (e.metadata.tags) {
e.metadata.tags.forEach(t => tags.add(t));
if (e.spec?.type) {
categories.add(e.spec.type as string);
}
});
return Array.from(tags).sort();
return Array.from(categories).sort();
}
// Given all filter groups and what filters are actually selected, extract all
@@ -220,7 +220,7 @@ function collectTags(entities?: Entity[]): string[] {
function buildMatchingEntities(
filterGroups: { [filterGroupId: string]: FilterGroup },
selectedFilterKeys: { [filterGroupId: string]: Set<string> },
selectedTags: string[],
selectedCategories: string[],
entities?: Entity[],
excludeFilterGroupId?: string,
): Entity[] {
@@ -247,13 +247,10 @@ function buildMatchingEntities(
}
}
// Filter by tags, if at least one tag is selected. Include all entities
// that have at least one of the selected tags
if (selectedTags.length > 0) {
allFilters.push(
entity =>
!!entity.metadata.tags &&
entity.metadata.tags.some(t => selectedTags.includes(t)),
// Filter by categories, if at least one category is selected.
if (selectedCategories.length > 0) {
allFilters.push(entity =>
selectedCategories.some(c => entity.spec?.type === c),
);
}
+2 -2
View File
@@ -26,13 +26,13 @@ export type FilterGroupsContext = {
) => void;
unregister: (filterGroupId: string) => void;
setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void;
setSelectedTags: (tags: string[]) => void;
setSelectedCategories: (categories: string[]) => void;
reload: () => Promise<void>;
loading: boolean;
error?: Error;
filterGroupStates: { [filterGroupId: string]: FilterGroupStates };
matchingEntities: Entity[];
availableTags: string[];
availableCategories: string[];
isCatalogEmpty: boolean;
};
@@ -30,7 +30,7 @@ export function useFilteredEntities() {
loading: context.loading,
error: context.error,
matchingEntities: context.matchingEntities,
availableTags: context.availableTags,
availableCategories: context.availableCategories,
isCatalogEmpty: context.isCatalogEmpty,
reload: context.reload,
};