From 5f97433a9db061ae541bf953d22e8aa459212724 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 24 Jun 2021 20:06:36 -0600 Subject: [PATCH] Remove deprecated code Signed-off-by: Tim Hansen --- .../ResultsFilter/ResultsFilter.test.tsx | 109 ------- .../ResultsFilter/ResultsFilter.tsx | 121 -------- .../ScaffolderFilter.test.tsx | 271 ------------------ .../ScaffolderFilter/ScaffolderFilter.tsx | 211 -------------- .../src/components/ScaffolderFilter/index.ts | 17 -- .../ScaffolderPage/ScaffolderPage.tsx | 1 - .../src/filter/EntityFilterGroupsProvider.tsx | 263 ----------------- plugins/scaffolder/src/filter/context.ts | 45 --- plugins/scaffolder/src/filter/index.ts | 28 -- plugins/scaffolder/src/filter/types.ts | 53 ---- .../src/filter/useEntityFilterGroup.test.tsx | 124 -------- .../src/filter/useEntityFilterGroup.ts | 69 ----- .../src/filter/useFilteredEntities.ts | 37 --- 13 files changed, 1349 deletions(-) delete mode 100644 plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx delete mode 100644 plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx delete mode 100644 plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx delete mode 100644 plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx delete mode 100644 plugins/scaffolder/src/components/ScaffolderFilter/index.ts delete mode 100644 plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx delete mode 100644 plugins/scaffolder/src/filter/context.ts delete mode 100644 plugins/scaffolder/src/filter/index.ts delete mode 100644 plugins/scaffolder/src/filter/types.ts delete mode 100644 plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx delete mode 100644 plugins/scaffolder/src/filter/useEntityFilterGroup.ts delete mode 100644 plugins/scaffolder/src/filter/useFilteredEntities.ts diff --git a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx deleted file mode 100644 index 3b85125da0..0000000000 --- a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { CatalogApi } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; -import React from 'react'; -import { EntityFilterGroupsProvider } from '../../filter'; -import { ResultsFilter } from './ResultsFilter'; - -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { - IdentityApi, - identityApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; - -describe('Results Filter', () => { - const catalogApi: Partial = { - getEntities: () => - Promise.resolve({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity1', - tags: ['java'], - }, - spec: { - owner: 'tools@example.com', - type: 'service', - }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity2', - }, - spec: { - owner: 'not-tools@example.com', - type: 'service', - }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity3', - tags: ['java', 'test'], - }, - spec: { - owner: 'tools@example.com', - type: 'service', - }, - }, - ] as Entity[], - }), - }; - - const identityApi: Partial = { - getUserId: () => 'tools@example.com', - }; - - const renderWrapped = (children: React.ReactNode) => - render( - wrapInTestApp( - - {children}, - , - ), - ); - - it('should render all available categories', async () => { - const categories = ['test', 'java']; - const { findByText } = renderWrapped( - , - ); - for (const category of categories) { - expect( - await findByText( - category.charAt(0).toLocaleUpperCase('en-US') + category.slice(1), - ), - ).toBeInTheDocument(); - } - }); -}); diff --git a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx deleted file mode 100644 index 3386b03cf7..0000000000 --- a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { - Button, - Checkbox, - Divider, - List, - ListItem, - ListItemText, - makeStyles, - Theme, - Typography, -} from '@material-ui/core'; -import React, { useContext } from 'react'; -import { filterGroupsContext } from '../../filter/context'; - -const useStyles = makeStyles(theme => ({ - filterBox: { - display: 'flex', - margin: theme.spacing(2, 0, 0, 0), - }, - filterBoxTitle: { - margin: theme.spacing(1, 0, 0, 1), - fontWeight: 'bold', - flex: 1, - }, - title: { - margin: theme.spacing(1, 0, 0, 1), - textTransform: 'uppercase', - fontSize: 12, - fontWeight: 'bold', - }, - checkbox: { - padding: theme.spacing(0, 1, 0, 1), - }, -})); - -type Props = { - availableCategories: string[]; -}; - -/** - * The additional results filter in the sidebar. - */ -export const ResultsFilter = ({ availableCategories }: Props) => { - const classes = useStyles(); - - const context = useContext(filterGroupsContext); - if (!context) { - throw new Error(`Must be used inside an EntityFilterGroupsProvider`); - } - - const { selectedCategories, setSelectedCategories } = context; - - return ( - <> -
- - Refine Results - {' '} - -
- - - Categories - - - {availableCategories.map(category => { - const labelId = `checkbox-list-label-${category}`; - return ( - - setSelectedCategories( - selectedCategories.includes(category) - ? selectedCategories.filter( - selectedCategory => selectedCategory !== category, - ) - : [...selectedCategories, category], - ) - } - > - - - - ); - })} - - - ); -}; diff --git a/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx b/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx deleted file mode 100644 index 5875646fae..0000000000 --- a/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 from 'react'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; -import { fireEvent, render, waitFor } from '@testing-library/react'; -import { CatalogApi } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { EntityFilterGroupsProvider } from '../../filter'; -import { ButtonGroup, ScaffolderFilter } from './ScaffolderFilter'; - -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { - IdentityApi, - identityApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; - -describe('Catalog Filter', () => { - const catalogApi: Partial = { - getEntities: () => - Promise.resolve({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity1', - }, - spec: { - owner: 'tools@example.com', - type: 'service', - }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity2', - }, - spec: { - owner: 'not-tools@example.com', - type: 'service', - }, - }, - ] as Entity[], - }), - }; - - const identityApi: Partial = { - getUserId: () => 'tools@example.com', - }; - - const renderWrapped = (children: React.ReactNode) => - render( - wrapInTestApp( - - {children}, - , - ), - ); - - describe('filter groups', () => { - it('should render the different groups', async () => { - 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(); - } - }); - }); - - describe('filter items', () => { - it('should render the different items and their names', async () => { - const mockGroups: ButtonGroup[] = [ - { - name: 'Test Group 1', - items: [ - { - id: 'all', - label: 'First Label', - filterFn: () => true, - }, - { - id: 'starred', - label: 'Second Label', - filterFn: () => false, - }, - ], - }, - ]; - - const { findByText } = renderWrapped( - , - ); - - for (const item of mockGroups[0].items) { - expect(await findByText(item.label)).toBeInTheDocument(); - } - }); - - it('selects the first item if no desired initial one is set', 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: '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: ButtonGroup[] = [ - { - name: 'Test Group 1', - items: [ - { - id: 'all', - label: 'First Label', - filterFn: () => true, - }, - { - id: 'starred', - label: 'Second Label', - filterFn: () => false, - }, - ], - }, - ]; - - const onChange = jest.fn(); - - const { findByText } = renderWrapped( - , - ); - - await waitFor(() => { - expect(onChange).toHaveBeenLastCalledWith({ - id: 'all', - label: 'First Label', - }); - }); - - fireEvent.click(await findByText('Second Label')); - - await waitFor(() => { - expect(onChange).toHaveBeenLastCalledWith({ - id: 'starred', - label: 'Second Label', - }); - }); - }); - - it('displays match counts properly', async () => { - const mockGroups: ButtonGroup[] = [ - { - name: 'Test Group 1', - items: [ - { - 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/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx b/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx deleted file mode 100644 index 2637a1f95c..0000000000 --- a/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { Entity } from '@backstage/catalog-model'; -import { - Card, - List, - ListItemIcon, - ListItemSecondaryAction, - ListItemText, - makeStyles, - MenuItem, - Theme, - Typography, -} from '@material-ui/core'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { FilterGroup, useEntityFilterGroup } from '../../filter'; -import { IconComponent } from '@backstage/core-plugin-api'; - -export type ButtonGroup = { - name: string; - items: { - id: string; - label: string; - icon?: IconComponent; - filterFn: (entity: Entity) => boolean; - }[]; -}; - -const useStyles = makeStyles(theme => ({ - root: { - backgroundColor: 'rgba(0, 0, 0, .11)', - boxShadow: 'none', - }, - title: { - margin: theme.spacing(1, 0, 0, 1), - textTransform: 'uppercase', - fontSize: 12, - fontWeight: 'bold', - }, - listIcon: { - minWidth: 30, - color: theme.palette.text.primary, - }, - menuItem: { - minHeight: theme.spacing(6), - }, - groupWrapper: { - margin: theme.spacing(1, 1, 2, 1), - }, - menuTitle: { - fontWeight: 500, - }, -})); - -type OnChangeCallback = (item: { id: string; label: string }) => void; - -type Props = { - buttonGroups: ButtonGroup[]; - initiallySelected: string; - onChange?: OnChangeCallback; -}; - -/** - * The main filter group in the sidebar, toggling owned/starred/all. - */ -export const ScaffolderFilter = ({ - buttonGroups, - onChange, - initiallySelected, -}: Props) => { - const classes = useStyles(); - const { currentFilter, setCurrentFilter, getFilterCount } = useFilter( - buttonGroups, - initiallySelected, - ); - - const onChangeRef = useRef(); - useEffect(() => { - onChangeRef.current = onChange; - }, [onChange]); - - const setCurrent = useCallback( - (item: { id: string; label: string }) => { - setCurrentFilter(item.id); - onChangeRef.current?.({ id: item.id, label: item.label }); - }, - [setCurrentFilter], - ); - - // Make one initial onChange to inform the surroundings about the selected - // item - useEffect(() => { - const items = buttonGroups.flatMap(g => g.items); - const item = items.find(i => i.id === initiallySelected) || items[0]; - if (item) { - onChangeRef.current?.({ id: item.id, label: item.label }); - } - // intentionally only happens on startup - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - {buttonGroups.map(group => ( - - - {group.name} - - - - {group.items.map(item => ( - setCurrent(item)} - selected={item.id === currentFilter} - className={classes.menuItem} - > - {item.icon && ( - - - - )} - - - {item.label} - - - - {getFilterCount(item.id) ?? '-'} - - - ))} - - - - ))} - - ); -}; - -function useFilter( - buttonGroups: ButtonGroup[], - initiallySelected: string, -): { - currentFilter: string; - setCurrentFilter: (filterId: string) => void; - getFilterCount: (filterId: string) => number | undefined; -} { - const [currentFilter, setCurrentFilter] = useState(initiallySelected); - - 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, - [initiallySelected], - ); - - const setCurrent = useCallback( - (filterId: string) => { - setCurrentFilter(filterId); - setSelectedFilters([filterId]); - }, - [setCurrentFilter, setSelectedFilters], - ); - - const getFilterCount = useCallback( - (filterId: string) => { - if (state.type !== 'ready') { - return undefined; - } - return state.state.filters[filterId].matchCount; - }, - [state], - ); - - return { - currentFilter, - setCurrentFilter: setCurrent, - getFilterCount, - }; -} diff --git a/plugins/scaffolder/src/components/ScaffolderFilter/index.ts b/plugins/scaffolder/src/components/ScaffolderFilter/index.ts deleted file mode 100644 index 6fbadb81fa..0000000000 --- a/plugins/scaffolder/src/components/ScaffolderFilter/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { ScaffolderFilter } from './ScaffolderFilter'; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index fae0a906cd..8881b69460 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -95,7 +95,6 @@ export const ScaffolderPageContents = () => { availableFilters={['all', 'starred']} /> {/* TODO(mtlewis) replace with custom checkbox list? maybe multiselect */} - {/* TODO(mtlewis) delete removed components since they're no longer used */} {/* TODO(mtlewis) consider adding tag picker? */} diff --git a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx deleted file mode 100644 index 7aa1a21643..0000000000 --- a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useAsyncFn } from 'react-use'; -import { filterGroupsContext, FilterGroupsContext } from './context'; -import { - EntityFilterFn, - FilterGroup, - FilterGroupState, - FilterGroupStates, -} from './types'; -import { useApi } from '@backstage/core-plugin-api'; - -/** - * Implementation of the shared filter groups state. - */ -export const EntityFilterGroupsProvider = ({ - children, -}: { - children?: React.ReactNode; -}) => { - const state = useProvideEntityFilters(); - return ( - - {children} - - ); -}; - -// The hook that implements the actual context building -function useProvideEntityFilters(): FilterGroupsContext { - const catalogApi = useApi(catalogApiRef); - const [{ value: entities, error }, doReload] = useAsyncFn(async () => { - const response = await catalogApi.getEntities({ - filter: { kind: 'Template' }, - }); - return response.items as TemplateEntityV1alpha1[]; - }); - - const filterGroups = useRef<{ - [filterGroupId: string]: FilterGroup; - }>({}); - const selectedFilterKeys = useRef<{ - [filterGroupId: string]: Set; - }>({}); - const selectedCategories = useRef([]); - const [filterGroupStates, setFilterGroupStates] = useState<{ - [filterGroupId: string]: FilterGroupStates; - }>({}); - const [filteredEntities, setFilteredEntities] = useState< - TemplateEntityV1alpha1[] - >([]); - const [availableCategories, setAvailableCategories] = useState([]); - const [isCatalogEmpty, setCatalogEmpty] = useState(false); - - useEffect(() => { - doReload(); - }, [doReload]); - - const rebuild = useCallback(() => { - setFilterGroupStates( - buildStates( - filterGroups.current, - selectedFilterKeys.current, - selectedCategories.current, - entities, - error, - ), - ); - setFilteredEntities( - buildMatchingEntities( - filterGroups.current, - selectedFilterKeys.current, - selectedCategories.current, - entities, - ), - ); - setAvailableCategories(collectCategories(entities)); - setCatalogEmpty(entities !== undefined && entities.length === 0); - }, [entities, error]); - - const register = useCallback( - ( - filterGroupId: string, - filterGroup: FilterGroup, - initialSelectedFilterIds?: string[], - ) => { - filterGroups.current[filterGroupId] = filterGroup; - selectedFilterKeys.current[filterGroupId] = new Set( - initialSelectedFilterIds ?? [], - ); - rebuild(); - }, - [rebuild], - ); - - const unregister = useCallback( - (filterGroupId: string) => { - delete filterGroups.current[filterGroupId]; - delete selectedFilterKeys.current[filterGroupId]; - rebuild(); - }, - [rebuild], - ); - - const setGroupSelectedFilters = useCallback( - (filterGroupId: string, filters: string[]) => { - selectedFilterKeys.current[filterGroupId] = new Set(filters); - rebuild(); - }, - [rebuild], - ); - - const setSelectedCategories = useCallback( - (categories: string[]) => { - selectedCategories.current = categories; - rebuild(); - }, - [rebuild], - ); - - const reload = useCallback(async () => { - await doReload(); - }, [doReload]); - - return { - register, - unregister, - setGroupSelectedFilters, - setSelectedCategories, - reload, - selectedCategories: selectedCategories.current, - loading: !error && !entities, - error, - filterGroupStates, - filteredEntities, - availableCategories, - isCatalogEmpty, - }; -} - -// Given all filter groups and what filters are actually selected, along with -// the loading state for entities, generate the state of each individual filter -function buildStates( - filterGroups: { [filterGroupId: string]: FilterGroup }, - selectedFilterKeys: { [filterGroupId: string]: Set }, - selectedCategories: string[], - entities?: TemplateEntityV1alpha1[], - error?: Error, -): { [filterGroupId: string]: FilterGroupStates } { - // On error - all entries are an error state - if (error) { - return Object.fromEntries( - Object.keys(filterGroups).map(filterGroupId => [ - filterGroupId, - { type: 'error', error }, - ]), - ); - } - - // On startup - all entries are a loading state - if (!entities) { - return Object.fromEntries( - Object.keys(filterGroups).map(filterGroupId => [ - filterGroupId, - { type: 'loading' }, - ]), - ); - } - - const result: { [filterGroupId: string]: FilterGroupStates } = {}; - for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) { - const otherMatchingEntities = buildMatchingEntities( - filterGroups, - selectedFilterKeys, - selectedCategories, - entities, - filterGroupId, - ); - const groupState: FilterGroupState = { filters: {} }; - for (const [filterId, filterFn] of Object.entries(filterGroup.filters)) { - const isSelected = !!selectedFilterKeys[filterGroupId]?.has(filterId); - const matchCount = otherMatchingEntities.filter(entity => - filterFn(entity), - ).length; - groupState.filters[filterId] = { isSelected, matchCount }; - } - result[filterGroupId] = { type: 'ready', state: groupState }; - } - - return result; -} - -// Given all entites, find all possible categories and provide them in a sorted list. -function collectCategories(entities?: TemplateEntityV1alpha1[]): string[] { - const categories = new Set(); - (entities || []).forEach(e => { - if (e.spec?.type) { - categories.add(e.spec.type as string); - } - }); - return Array.from(categories).sort(); -} - -// Given all filter groups and what filters are actually selected, extract all -// entities that match all those filter groups. -function buildMatchingEntities( - filterGroups: { [filterGroupId: string]: FilterGroup }, - selectedFilterKeys: { [filterGroupId: string]: Set }, - selectedCategories: string[], - entities?: TemplateEntityV1alpha1[], - excludeFilterGroupId?: string, -): TemplateEntityV1alpha1[] { - // Build one filter fn per filter group - const allFilters: EntityFilterFn[] = []; - for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) { - if (excludeFilterGroupId === filterGroupId) { - continue; - } - - // Pick out all of the filter functions in the group that are actually - // selected - const groupFilters: EntityFilterFn[] = []; - for (const [filterId, filterFn] of Object.entries(filterGroup.filters)) { - if (!!selectedFilterKeys[filterGroupId]?.has(filterId)) { - groupFilters.push(filterFn); - } - } - - // Need to match any of the selected filters in the group - if there is - // any at all - if (groupFilters.length) { - allFilters.push(entity => groupFilters.some(fn => fn(entity))); - } - } - - // Filter by categories, if at least one category is selected. - if (selectedCategories.length > 0) { - allFilters.push(entity => - selectedCategories.some(c => entity.spec?.type === c), - ); - } - - // All filter groups that had any checked filters need to match. Note that - // every() always returns true for an empty array. - return entities?.filter(entity => allFilters.every(fn => fn(entity))) ?? []; -} diff --git a/plugins/scaffolder/src/filter/context.ts b/plugins/scaffolder/src/filter/context.ts deleted file mode 100644 index ee66e3d9da..0000000000 --- a/plugins/scaffolder/src/filter/context.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { createContext } from 'react'; -import { FilterGroup, FilterGroupStates } from './types'; - -export type FilterGroupsContext = { - register: ( - filterGroupId: string, - filterGroup: FilterGroup, - initialSelectedFilterIds?: string[], - ) => void; - unregister: (filterGroupId: string) => void; - setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; - setSelectedCategories: (categories: string[]) => void; - reload: () => Promise; - selectedCategories: string[]; - loading: boolean; - error?: Error; - filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; - filteredEntities: TemplateEntityV1alpha1[]; - availableCategories: string[]; - isCatalogEmpty: boolean; -}; - -/** - * The context that maintains shared state for all visible filter groups. - */ -export const filterGroupsContext = createContext< - FilterGroupsContext | undefined ->(undefined); diff --git a/plugins/scaffolder/src/filter/index.ts b/plugins/scaffolder/src/filter/index.ts deleted file mode 100644 index bed464222f..0000000000 --- a/plugins/scaffolder/src/filter/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider'; -export type { - EntityFilterFn, - FilterGroup, - FilterGroupState, - FilterGroupStates, - FilterGroupStatesError, - FilterGroupStatesLoading, - FilterGroupStatesReady, -} from './types'; -export { useEntityFilterGroup } from './useEntityFilterGroup'; -export { useFilteredEntities } from './useFilteredEntities'; diff --git a/plugins/scaffolder/src/filter/types.ts b/plugins/scaffolder/src/filter/types.ts deleted file mode 100644 index 284ca608f1..0000000000 --- a/plugins/scaffolder/src/filter/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { Entity } from '@backstage/catalog-model'; - -export type EntityFilterFn = (entity: Entity) => boolean; - -export type FilterGroup = { - filters: { - [filterId: string]: EntityFilterFn; - }; -}; - -export type FilterGroupState = { - filters: { - [filterId: string]: { - isSelected: boolean; - matchCount: number; - }; - }; -}; - -export type FilterGroupStatesReady = { - type: 'ready'; - state: FilterGroupState; -}; - -export type FilterGroupStatesError = { - type: 'error'; - error: Error; -}; - -export type FilterGroupStatesLoading = { - type: 'loading'; -}; - -export type FilterGroupStates = - | FilterGroupStatesReady - | FilterGroupStatesError - | FilterGroupStatesLoading; diff --git a/plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx b/plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx deleted file mode 100644 index 5a03ecb425..0000000000 --- a/plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { MockStorageApi } from '@backstage/test-utils'; -import { act, renderHook } from '@testing-library/react-hooks'; -import React from 'react'; -import { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider'; -import { FilterGroup, FilterGroupStatesReady } from './types'; -import { useEntityFilterGroup } from './useEntityFilterGroup'; - -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { storageApiRef } from '@backstage/core-plugin-api'; - -describe('useEntityFilterGroup', () => { - let catalogApi: jest.Mocked; - let wrapper: ({ children }: { children?: React.ReactNode }) => JSX.Element; - - beforeEach(() => { - catalogApi = { - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - addLocation: jest.fn(_a => new Promise(() => {})), - getEntities: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), - getLocationById: jest.fn(), - removeLocationById: jest.fn(), - removeEntityByUid: jest.fn(), - getEntityByName: jest.fn(), - }; - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - storageApiRef, - MockStorageApi.create(), - ); - wrapper = ({ children }: { children?: React.ReactNode }) => ( - - {children} - - ); - }); - - it('works for an empty set of filters', async () => { - catalogApi.getEntities.mockResolvedValue({ items: [] }); - const group: FilterGroup = { filters: {} }; - const { result, waitFor } = renderHook( - () => useEntityFilterGroup('g1', group), - { wrapper }, - ); - - await waitFor(() => expect(result.current.state.type).toBe('ready')); - }); - - it('works for a single group', async () => { - catalogApi.getEntities.mockResolvedValue({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { name: 'n' }, - }, - ], - }); - const group: FilterGroup = { - filters: { - f1: e => e.metadata.name === 'n', - f2: e => e.metadata.name !== 'n', - }, - }; - const { result, waitFor } = renderHook( - () => useEntityFilterGroup('g1', group), - { wrapper }, - ); - - await waitFor(() => expect(result.current.state.type).toEqual('ready')); - let state = result.current.state as FilterGroupStatesReady; - expect(state.state.filters.f1).toEqual({ - isSelected: false, - matchCount: 1, - }); - expect(state.state.filters.f2).toEqual({ - isSelected: false, - matchCount: 0, - }); - - act(() => result.current.setSelectedFilters(['f1'])); - - await waitFor(() => expect(result.current.state.type).toEqual('ready')); - state = result.current.state as FilterGroupStatesReady; - expect(state.state.filters.f1).toEqual({ - isSelected: true, - matchCount: 1, - }); - expect(state.state.filters.f2).toEqual({ - isSelected: false, - matchCount: 0, - }); - - act(() => result.current.setSelectedFilters(['f2'])); - - await waitFor(() => expect(result.current.state.type).toEqual('ready')); - state = result.current.state as FilterGroupStatesReady; - expect(state.state.filters.f1).toEqual({ - isSelected: false, - matchCount: 1, - }); - expect(state.state.filters.f2).toEqual({ - isSelected: true, - matchCount: 0, - }); - }); -}); diff --git a/plugins/scaffolder/src/filter/useEntityFilterGroup.ts b/plugins/scaffolder/src/filter/useEntityFilterGroup.ts deleted file mode 100644 index 13857724cf..0000000000 --- a/plugins/scaffolder/src/filter/useEntityFilterGroup.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { useCallback, useContext, useEffect, useMemo } from 'react'; -import { filterGroupsContext } from './context'; -import { FilterGroup, FilterGroupStates } from './types'; - -export type EntityFilterGroupOutput = { - state: FilterGroupStates; - setSelectedFilters: (filterIds: string[]) => void; -}; - -/** - * Hook that exposes the relevant data and operations for a single filter - * group. - */ -export const useEntityFilterGroup = ( - filterGroupId: string, - filterGroup: FilterGroup, - initialSelectedFilters?: string[], -): EntityFilterGroupOutput => { - const context = useContext(filterGroupsContext); - if (!context) { - throw new Error(`Must be used inside an EntityFilterGroupsProvider`); - } - const { - register, - unregister, - setGroupSelectedFilters, - filterGroupStates, - } = context; - - // Intentionally consider initial set only at mount time - // eslint-disable-next-line react-hooks/exhaustive-deps - const initialMemo = useMemo(() => initialSelectedFilters?.slice(), []); - - // Register the group on mount, and unregister on unmount - useEffect(() => { - register(filterGroupId, filterGroup, initialMemo); - return () => unregister(filterGroupId); - }, [register, unregister, filterGroupId, filterGroup, initialMemo]); - - const setSelectedFilters = useCallback( - (filters: string[]) => { - setGroupSelectedFilters(filterGroupId, filters); - }, - [setGroupSelectedFilters, filterGroupId], - ); - - let state = filterGroupStates[filterGroupId]; - if (!state) { - state = { type: 'loading' }; - } - - return { state, setSelectedFilters }; -}; diff --git a/plugins/scaffolder/src/filter/useFilteredEntities.ts b/plugins/scaffolder/src/filter/useFilteredEntities.ts deleted file mode 100644 index e091316d11..0000000000 --- a/plugins/scaffolder/src/filter/useFilteredEntities.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { useContext } from 'react'; -import { filterGroupsContext } from './context'; - -/** - * Hook that exposes the result of applying a set of filter groups. - */ -export function useFilteredEntities() { - const context = useContext(filterGroupsContext); - if (!context) { - throw new Error(`Must be used inside an EntityFilterGroupsProvider`); - } - - return { - loading: context.loading, - error: context.error, - filteredEntities: context.filteredEntities, - availableCategories: context.availableCategories, - isCatalogEmpty: context.isCatalogEmpty, - reload: context.reload, - }; -}