diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 61a83fccd8..e40f4806ed 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -20,6 +20,7 @@ import { List, ListItemIcon, ListItemText, + ListItemSecondaryAction, MenuItem, Typography, Theme, @@ -109,7 +110,9 @@ export const CatalogFilter: FC<{ {item.label} - {entitiesByFilter[item.id]?.length ?? '-'} + + {entitiesByFilter[item.id]?.length ?? '-'} + ))} diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ef77cf573f..9329eb9317 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -162,7 +162,7 @@ export const CatalogPage: FC<{}> = () => { color="primary" to={scaffolderRootRoute.path} > - Create Service + Create Component All your software catalog entities diff --git a/plugins/catalog/src/hooks/useEntities.test.tsx b/plugins/catalog/src/hooks/useEntities.test.tsx new file mode 100644 index 0000000000..c8176f3a6c --- /dev/null +++ b/plugins/catalog/src/hooks/useEntities.test.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useEntityFilterGroup } from './useEntities'; + +describe('useEntitiesHooks', () => { + const testEntities = [ + { name: 'test1', type: 'type1' }, + { name: 'test2', type: 'type2' }, + { name: 'test3', type: 'type3' }, + { name: 'test4', type: 'type2' }, + { name: 'test5', type: 'type2' }, + ]; + + type TestEntitiy = { + name: string; + type: string; + }; + + const testFilterFunctions = { + type1: { + filterFunction: (entity: TestEntitiy) => entity.type === 'type1', + isSelected: false, + }, + type2: { + filterFunction: (entity: TestEntitiy) => entity.type === 'type2', + isSelected: false, + }, + type3: { + filterFunction: (entity: TestEntitiy) => entity.type === 'type3', + isSelected: false, + }, + }; + + it('should calculate count', async () => { + const { result } = renderHook(() => + useEntityFilterGroup(testEntities, testFilterFunctions), + ); + + expect(result.current.states.type1.count).toBe(1); + expect(result.current.states.type2.count).toBe(3); + expect(result.current.states.type3.count).toBe(1); + }); + + it('should set the isSelected flag properly', () => { + const { result } = renderHook(() => + useEntityFilterGroup(testEntities, testFilterFunctions), + ); + + expect(result.current.states.type1.isSelected).toBeFalsy(); + expect(result.current.states.type2.isSelected).toBeFalsy(); + expect(result.current.states.type3.isSelected).toBeFalsy(); + + act(() => { + result.current.selectItems(['type1']); + }); + + expect(result.current.states.type1.isSelected).toBeTruthy(); + expect(result.current.states.type2.isSelected).toBeFalsy(); + expect(result.current.states.type3.isSelected).toBeFalsy(); + }); + + it('should filter entities', () => { + const { result } = renderHook(() => + useEntityFilterGroup(testEntities, testFilterFunctions), + ); + + act(() => { + result.current.selectItems(['type1']); + }); + expect(result.current.filteredItems).toEqual([ + { name: 'test1', type: 'type1' }, + ]); + + act(() => { + result.current.selectItems(['type2']); + }); + expect(result.current.filteredItems).toEqual([ + { name: 'test2', type: 'type2' }, + { name: 'test4', type: 'type2' }, + { name: 'test5', type: 'type2' }, + ]); + + act(() => { + result.current.selectItems(['type3', 'type1']); + }); + expect(result.current.filteredItems).toEqual([ + { name: 'test1', type: 'type1' }, + { name: 'test3', type: 'type3' }, + ]); + }); +}); diff --git a/plugins/catalog/src/hooks/useEntities.ts b/plugins/catalog/src/hooks/useEntities.ts index 6509bd9b4b..c48e3af026 100644 --- a/plugins/catalog/src/hooks/useEntities.ts +++ b/plugins/catalog/src/hooks/useEntities.ts @@ -40,6 +40,88 @@ type UseEntities = { selectTypeFilter: (id: string) => void; }; +type EntityFilterGroupOutput = { + selectItems: (items: string[]) => void; + filteredItems: T[]; + states: OutputState; +}; + +type OutputState = { [key: string]: { isSelected: boolean; count: number } }; + +type FilterDefinition = { + [key: string]: { + isSelected: boolean; + filterFunction: (entity: T) => boolean; + }; +}; + +export const useEntityFilterGroup = ( + entities: T[], + filterFunctions: FilterDefinition, +): EntityFilterGroupOutput => { + const [filterFuncs, setFilterFuncs] = useState>( + filterFunctions, + ); + + // and + // Object.entries(filterFuncs).filter(([_, {isSelected}]) => isSelected).map(([_, {filterFunction}]) => filterFunction).reduce((acc, func) => (acc.filter(func)), entities) + + return { + selectItems: (functionNames: Array) => { + const selectedFilterFunctions = Object.fromEntries( + Object.entries(filterFunctions).map(([key, { filterFunction }]) => [ + key, + { isSelected: functionNames.includes(key), filterFunction }, + ]), + ); + setFilterFuncs(selectedFilterFunctions); + }, + filteredItems: entities.filter(entity => + Object.entries(filterFuncs) + .filter(([_, { isSelected }]) => isSelected) + .map(([_, { filterFunction }]) => filterFunction) + .map(filter => filter(entity)) + .some(v => v === true), + ), + states: Object.keys(filterFuncs).reduce( + (acc, val) => ({ + ...acc, + [val]: { + ...filterFuncs[val], + count: entities.filter(filterFuncs[val].filterFunction).length, + }, + }), + {} as OutputState, + ), + }; +}; + +// const MyFilterGroup = () => { +// const { selectedItems, selectItems, counts } = useEntityFilterGroup( +// 'lifecycle', +// { +// production: e => e.spec?.lifecyle === 'production', +// }, +// ); + +// return ( +// +// selectItem('production')} +// > +// Production ({counts.production}) +// +// +// ); +// }; + +export const useUser = () => { + const indentityApi = useApi(identityApiRef); + const userId = indentityApi.getUserId(); + return { userId }; +}; + export const useEntities = (): UseEntities => { const [selectedFilter, setSelectedFilter] = useState< EntityGroup | undefined @@ -51,8 +133,7 @@ export const useEntities = (): UseEntities => { async () => catalogApi.getEntities(), ); - const indentityApi = useApi(identityApiRef); - const userId = indentityApi.getUserId(); + const { userId } = useUser(); const [selectedTypeFilter, selectTypeFilter] = useState( labeledEntityTypes[0].id,