Scaffolder: Replace Tag Filter with Category Filter
This commit is contained in:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
+7
-7
@@ -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();
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user