From 52e784f6516c57766072303c3bafc5ad9cd338fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 27 Jun 2020 22:33:59 +0200 Subject: [PATCH] Supply the sidebar catalog filter from the page again --- .../CatalogFilter/CatalogFilter.test.tsx | 124 ++++++++++-------- .../CatalogFilter/CatalogFilter.tsx | 77 +++++------ .../components/CatalogPage/CatalogPage.tsx | 104 +++++++++++---- plugins/catalog/src/data/filters.ts | 67 ---------- 4 files changed, 180 insertions(+), 192 deletions(-) delete mode 100644 plugins/catalog/src/data/filters.ts diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index 9dd0caa3b4..a98eec0875 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -26,9 +26,8 @@ import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { CatalogApi, catalogApiRef } from '../../api/types'; -import { EntityGroup } from '../../data/filters'; import { EntityFilterGroupsProvider } from '../../filter'; -import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; +import { ButtonGroup, CatalogFilter } from './CatalogFilter'; describe('Catalog Filter', () => { const catalogApi: Partial = { @@ -79,12 +78,12 @@ describe('Catalog Filter', () => { ); it('should render the different groups', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [] }, { name: 'Test Group 2', items: [] }, ]; const { findByText } = renderWrapped( - , + , ); for (const group of mockGroups) { expect(await findByText(group.name)).toBeInTheDocument(); @@ -92,24 +91,26 @@ describe('Catalog Filter', () => { }); it('should render the different items and their names', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.ALL, + id: 'all', label: 'First Label', + filterFn: () => true, }, { - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', + filterFn: () => false, }, ], }, ]; const { findByText } = renderWrapped( - , + , ); for (const item of mockGroups[0].items) { @@ -118,48 +119,19 @@ describe('Catalog Filter', () => { }); it('selects the first item if no desired initial one is set', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.ALL, + id: 'all', label: 'First Label', + filterFn: () => true, }, { - id: EntityGroup.STARRED, - label: 'Second Label', - }, - ], - }, - ]; - - const onChange = jest.fn(); - - renderWrapped( - , - ); - - await waitFor(() => { - expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.ALL, - label: 'First Label', - }); - }); - }); - - it('selects the initial item', async () => { - const mockGroups: CatalogFilterGroup[] = [ - { - name: 'Test Group 1', - items: [ - { - id: EntityGroup.ALL, - label: 'First Label', - }, - { - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', + filterFn: () => false, }, ], }, @@ -169,32 +141,71 @@ describe('Catalog Filter', () => { renderWrapped( , ); await waitFor(() => { expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.STARRED, + id: 'all', + label: 'First Label', + }); + }); + }); + + it('selects the initial item', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'all', + label: 'First Label', + filterFn: () => true, + }, + { + id: 'starred', + label: 'Second Label', + filterFn: () => false, + }, + ], + }, + ]; + + const onChange = jest.fn(); + + renderWrapped( + , + ); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith({ + id: 'starred', label: 'Second Label', }); }); }); it('can change the selected item', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.ALL, + id: 'all', label: 'First Label', + filterFn: () => true, }, { - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', + filterFn: () => false, }, ], }, @@ -203,12 +214,16 @@ describe('Catalog Filter', () => { const onChange = jest.fn(); const { findByText } = renderWrapped( - , + , ); await waitFor(() => { expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.ALL, + id: 'all', label: 'First Label', }); }); @@ -217,27 +232,28 @@ describe('Catalog Filter', () => { await waitFor(() => { expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', }); }); }); it('displays match counts properly', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.OWNED, + id: 'owned', label: 'First Label', + filterFn: entity => entity.spec?.owner === 'tools@example.com', }, ], }, ]; const { findByText } = renderWrapped( - , + , ); expect(await findByText('1')).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index ea22198e6d..90a1e15ad0 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { IconComponent, identityApiRef, useApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { IconComponent } from '@backstage/core'; import { Card, List, @@ -27,31 +28,22 @@ import { Typography, } from '@material-ui/core'; import React, { - FC, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; -import { - EntityFilterOptions, - entityFilters, - EntityGroup, -} from '../../data/filters'; import { FilterGroup, useEntityFilterGroup } from '../../filter'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; -export type CatalogFilterItem = { - id: EntityGroup; - label: string; - icon?: IconComponent; - count?: number | FC; -}; - -export type CatalogFilterGroup = { +export type ButtonGroup = { name: string; - items: CatalogFilterItem[]; + items: { + id: string; + label: string; + icon?: IconComponent; + filterFn: (entity: Entity) => boolean; + }[]; }; const useStyles = makeStyles(theme => ({ @@ -83,21 +75,24 @@ const useStyles = makeStyles(theme => ({ type OnChangeCallback = (item: { id: string; label: string }) => void; type Props = { - filterGroups: CatalogFilterGroup[]; + buttonGroups: ButtonGroup[]; + initiallySelected: string; onChange?: OnChangeCallback; - initiallySelected?: EntityGroup; }; /** * The main filter group in the sidebar, toggling owned/starred/all. */ export const CatalogFilter = ({ - filterGroups, + buttonGroups, onChange, initiallySelected, }: Props) => { const classes = useStyles(); - const { currentFilter, setCurrentFilter, getFilterCount } = useFilter(); + const { currentFilter, setCurrentFilter, getFilterCount } = useFilter( + buttonGroups, + initiallySelected, + ); const onChangeRef = useRef(); useEffect(() => { @@ -105,9 +100,9 @@ export const CatalogFilter = ({ }, [onChange]); const setCurrent = useCallback( - (item: CatalogFilterItem) => { + (item: { id: string; label: string }) => { setCurrentFilter(item.id); - onChangeRef.current?.(item); + onChangeRef.current?.({ id: item.id, label: item.label }); }, [setCurrentFilter], ); @@ -115,10 +110,10 @@ export const CatalogFilter = ({ // Make one initial onChange to inform the surroundings about the selected // item useEffect(() => { - const items = filterGroups.flatMap(g => g.items); + const items = buttonGroups.flatMap(g => g.items); const item = items.find(i => i.id === initiallySelected) || items[0]; if (item) { - onChangeRef.current?.(item); + onChangeRef.current?.({ id: item.id, label: item.label }); } // intentionally only happens on startup // eslint-disable-next-line react-hooks/exhaustive-deps @@ -126,7 +121,7 @@ export const CatalogFilter = ({ return ( - {filterGroups.map(group => ( + {buttonGroups.map(group => ( {group.name} @@ -165,31 +160,29 @@ export const CatalogFilter = ({ ); }; -function useFilter(): { +function useFilter( + buttonGroups: ButtonGroup[], + initiallySelected: string, +): { currentFilter: string; setCurrentFilter: (filterId: string) => void; getFilterCount: (filterId: string) => number | undefined; } { - const [currentFilter, setCurrentFilter] = useState('OWNED'); - const { isStarredEntity } = useStarredEntities(); - const userId = useApi(identityApiRef).getUserId(); + const [currentFilter, setCurrentFilter] = useState(initiallySelected); - const filterGroup = useMemo(() => { - const result: FilterGroup = { filters: {} }; - const options: EntityFilterOptions = { - userId, - isStarred: isStarredEntity, - }; - for (const [filterId, filterFn] of Object.entries(entityFilters)) { - result.filters[filterId] = entity => filterFn(entity, options); - } - return result; - }, [isStarredEntity, userId]); + const filterGroup = useMemo( + () => ({ + filters: Object.fromEntries( + buttonGroups.flatMap(g => g.items).map(i => [i.id, i.filterFn]), + ), + }), + [buttonGroups], + ); const { setSelectedFilters, state } = useEntityFilterGroup( 'primary-sidebar', filterGroup, - ['OWNED'], + [initiallySelected], ); const setCurrent = useCallback( diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 4f85ff3fb4..e59b753363 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -14,14 +14,22 @@ * limitations under the License. */ -import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { + Content, + ContentHeader, + identityApiRef, + SupportButton, + useApi, +} from '@backstage/core'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles } from '@material-ui/core'; -import React, { useState } from 'react'; +import SettingsIcon from '@material-ui/icons/Settings'; +import StarIcon from '@material-ui/icons/Star'; +import React, { useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { EntityGroup, filterGroups } from '../../data/filters'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; -import { CatalogFilter } from '../CatalogFilter/CatalogFilter'; +import { useStarredEntities } from '../../hooks/useStarredEntites'; +import { CatalogFilter, ButtonGroup } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; import CatalogLayout from './CatalogLayout'; import { CatalogTabs, LabeledComponentType } from './CatalogTabs'; @@ -36,35 +44,73 @@ const useStyles = makeStyles(theme => ({ }, })); -const tabs: LabeledComponentType[] = [ - { - id: 'service', - label: 'Services', - }, - { - id: 'website', - label: 'Websites', - }, - { - id: 'library', - label: 'Libraries', - }, - { - id: 'documentation', - label: 'Documentation', - }, - { - id: 'other', - label: 'Other', - }, -]; - const CatalogPageContents = () => { const styles = useStyles(); const { loading, error, matchingEntities } = useFilteredEntities(); + const { isStarredEntity } = useStarredEntities(); + const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); const [selectedSidebarItem, setSelectedSidebarItem] = useState(); + const tabs = useMemo( + () => [ + { + id: 'service', + label: 'Services', + }, + { + id: 'website', + label: 'Websites', + }, + { + id: 'library', + label: 'Libraries', + }, + { + id: 'documentation', + label: 'Documentation', + }, + { + id: 'other', + label: 'Other', + }, + ], + [], + ); + + const filterGroups = useMemo( + () => [ + { + name: 'Personal', + items: [ + { + id: 'owned', + label: 'Owned', + icon: SettingsIcon, + filterFn: entity => entity.spec?.owner === userId, + }, + { + id: 'starred', + label: 'Starred', + icon: StarIcon, + filterFn: isStarredEntity, + }, + ], + }, + { + name: 'Company', // TODO: Replace with Company name, read from app config. + items: [ + { + id: 'all', + label: 'All', + filterFn: () => true, + }, + ], + }, + ], + [isStarredEntity, userId], + ); + return ( {
setSelectedSidebarItem(label)} - initiallySelected={EntityGroup.OWNED} + initiallySelected="owned" />
boolean; - -export type EntityFilterOptions = { - isStarred: (entity: Entity) => boolean; - userId: string; -}; - -export const entityFilters: Record = { - [EntityGroup.OWNED]: (e, { userId }) => e.spec?.owner === userId, - [EntityGroup.ALL]: () => true, - [EntityGroup.STARRED]: (e, { isStarred }) => isStarred(e), -};