diff --git a/.changeset/four-owls-raise.md b/.changeset/four-owls-raise.md new file mode 100644 index 0000000000..6e8824cb11 --- /dev/null +++ b/.changeset/four-owls-raise.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog': minor +'@backstage/plugin-catalog-react': minor +'@backstage/plugin-scaffolder': minor +--- + +Moved common useStarredEntities hook to plugin-catalog-react diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 2ecf6e9a90..d964c22a3b 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -16,3 +16,4 @@ export { EntityContext, useEntity, useEntityFromUrl } from './useEntity'; export { useEntityCompoundName } from './useEntityCompoundName'; export { useRelatedEntities } from './useRelatedEntities'; +export { useStarredEntities } from './useStarredEntities'; diff --git a/plugins/scaffolder/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx similarity index 100% rename from plugins/scaffolder/src/hooks/useStarredEntities.test.tsx rename to plugins/catalog-react/src/hooks/useStarredEntities.test.tsx diff --git a/plugins/catalog/src/hooks/useStarredEntities.ts b/plugins/catalog-react/src/hooks/useStarredEntities.ts similarity index 100% rename from plugins/catalog/src/hooks/useStarredEntities.ts rename to plugins/catalog-react/src/hooks/useStarredEntities.ts diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ed51316dcd..327bc2e472 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -23,14 +23,18 @@ import { useApi, useRouteRef, } from '@backstage/core'; -import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + isOwnerOf, + useStarredEntities, +} from '@backstage/plugin-catalog-react'; + import { Button, makeStyles } from '@material-ui/core'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; -import { useStarredEntities } from '../../hooks/useStarredEntities'; import { createComponentRouteRef } from '../../routes'; import { ButtonGroup, diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 91f187855a..12d97e91a6 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -32,13 +32,14 @@ import { EntityRefLinks, formatEntityRefTitle, getEntityRelations, + useStarredEntities, } from '@backstage/plugin-catalog-react'; + import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import React from 'react'; import { findLocationForEntityMeta } from '../../data/utils'; -import { useStarredEntities } from '../../hooks/useStarredEntities'; import { createEditLink } from '../createEditLink'; import { favouriteEntityIcon, diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx index 970ce32ece..1c414414aa 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -15,10 +15,10 @@ */ import React, { ComponentProps } from 'react'; +import { useStarredEntities } from '@backstage/plugin-catalog-react'; import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import StarBorder from '@material-ui/icons/StarBorder'; import Star from '@material-ui/icons/Star'; -import { useStarredEntities } from '../../hooks/useStarredEntities'; import { Entity } from '@backstage/catalog-model'; type Props = ComponentProps & { entity: Entity }; diff --git a/plugins/catalog/src/hooks/useStarredEntities.test.tsx b/plugins/catalog/src/hooks/useStarredEntities.test.tsx deleted file mode 100644 index 78d2c4a58f..0000000000 --- a/plugins/catalog/src/hooks/useStarredEntities.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 React, { PropsWithChildren } from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useStarredEntities } from './useStarredEntities'; -import { - ApiProvider, - ApiRegistry, - storageApiRef, - WebStorage, - StorageApi, -} from '@backstage/core'; -import { MockErrorApi } from '@backstage/test-utils'; -import { Entity } from '@backstage/catalog-model'; - -describe('useStarredEntities', () => { - let mockStorage: StorageApi | undefined; - - const mockEntity: Entity = { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'mock', - }, - }; - - const secondMockEntity: Entity = { - apiVersion: '1', - kind: 'Component', - metadata: { - namespace: 'test', - name: 'mock2', - }, - }; - - const wrapper = ({ children }: PropsWithChildren<{}>) => { - return ( - - {children} - - ); - }; - - beforeEach(() => { - mockStorage = new WebStorage('@backstage', new MockErrorApi()).forBucket( - Date.now().toString(), // TODO(blam): need something that changes every test run for now until the MockStorage is implemented - ); - }); - it('should return an empty set for when there is no items in storage', async () => { - const { result } = renderHook(() => useStarredEntities(), { wrapper }); - - expect(result.current.starredEntities.size).toBe(0); - }); - it('should return a set with the current items when there is items in storage', async () => { - const expectedIds = ['i', 'am', 'some', 'test', 'ids']; - const store = mockStorage?.forBucket('settings'); - await store?.set('starredEntities', expectedIds); - - const { result } = renderHook(() => useStarredEntities(), { wrapper }); - - for (const item of expectedIds) { - expect(result.current.starredEntities.has(item)).toBeTruthy(); - } - }); - it('should listen to changes when the storage is set elsewhere', async () => { - const { result, waitForNextUpdate } = renderHook( - () => useStarredEntities(), - { wrapper }, - ); - - expect(result.current.starredEntities.size).toBe(0); - expect(result.current.isStarredEntity(mockEntity)).toBeFalsy(); - - // Make this happen after awaiting for the next update so we can - // catch when the hook re-renders with the latest data - setTimeout(() => result.current.toggleStarredEntity(mockEntity), 1); - - await waitForNextUpdate(); - - expect(result.current.starredEntities.size).toBe(1); - expect(result.current.isStarredEntity(mockEntity)).toBeTruthy(); - }); - - it('should write new entries to the local store when adding a togglging entity', async () => { - const { result } = renderHook(() => useStarredEntities(), { wrapper }); - - act(() => { - result.current.toggleStarredEntity(mockEntity); - }); - - expect(result.current.isStarredEntity(mockEntity)).toBeTruthy(); - expect(result.current.isStarredEntity(secondMockEntity)).toBeFalsy(); - }); - - it('should remove an existing entity when toggling entries', async () => { - const { result } = renderHook(() => useStarredEntities(), { wrapper }); - - act(() => { - result.current.toggleStarredEntity(mockEntity); - result.current.toggleStarredEntity(secondMockEntity); - result.current.toggleStarredEntity(mockEntity); - }); - - expect(result.current.isStarredEntity(mockEntity)).toBeFalsy(); - expect(result.current.isStarredEntity(secondMockEntity)).toBeTruthy(); - }); -}); diff --git a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx index 19c88a87f7..301c6b01d8 100644 --- a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx +++ b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx @@ -25,7 +25,7 @@ import { Theme, Typography, } from '@material-ui/core'; -import React, { useCallback, useContext, useState } from 'react'; +import React, { useContext } from 'react'; import { filterGroupsContext } from '../../filter/context'; const useStyles = makeStyles(theme => ({ @@ -59,20 +59,12 @@ type Props = { export const ResultsFilter = ({ availableCategories }: Props) => { const classes = useStyles(); - const [selectedCategories, setSelectedCategories] = useState([]); const context = useContext(filterGroupsContext); if (!context) { throw new Error(`Must be used inside an EntityFilterGroupsProvider`); } - const setSelectedCatgoriesFilter = context?.setSelectedCategories; - const updateSelectedCategories = useCallback( - (categories: string[]) => { - setSelectedCategories(categories); - setSelectedCatgoriesFilter(categories); - }, - [setSelectedCategories, setSelectedCatgoriesFilter], - ); + const { selectedCategories, setSelectedCategories } = context; return ( <> @@ -80,7 +72,7 @@ export const ResultsFilter = ({ availableCategories }: Props) => { Refine Results {' '} - + @@ -95,7 +87,7 @@ export const ResultsFilter = ({ availableCategories }: Props) => { dense button onClick={() => - updateSelectedCategories( + setSelectedCategories( selectedCategories.includes(category) ? selectedCategories.filter( selectedCategory => selectedCategory !== category, diff --git a/plugins/scaffolder/src/components/ScaffolderFilter/AllServicesCount.tsx b/plugins/scaffolder/src/components/ScaffolderFilter/AllServicesCount.tsx deleted file mode 100644 index efacfa4320..0000000000 --- a/plugins/scaffolder/src/components/ScaffolderFilter/AllServicesCount.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { CircularProgress, useTheme } from '@material-ui/core'; -import React from 'react'; -import { useAsync } from 'react-use'; - -export const AllServicesCount = () => { - const theme = useTheme(); - const catalogApi = useApi(catalogApiRef); - const { value, loading } = useAsync(() => catalogApi.getEntities()); - - if (loading) { - return ; - } - - return {value ?? length ?? '-'}; -}; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 74582d0762..2a5f3309a0 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -15,6 +15,7 @@ */ import React, { useEffect, useMemo, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { configApiRef, @@ -28,15 +29,14 @@ import { useApi, WarningPanel, } from '@backstage/core'; +import { useStarredEntities } from '@backstage/plugin-catalog-react'; import { Button, Grid, Link, makeStyles, Typography } from '@material-ui/core'; -import { Link as RouterLink } from 'react-router-dom'; +import StarIcon from '@material-ui/icons/Star'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { TemplateCard, TemplateCardProps } from '../TemplateCard'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { ScaffolderFilter } from '../ScaffolderFilter'; import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter'; -import StarIcon from '@material-ui/icons/Star'; -import { useStarredEntities } from '../../hooks/useStarredEntities'; import SearchToolbar from '../SearchToolbar/SearchToolbar'; const useStyles = makeStyles(theme => ({ @@ -104,7 +104,7 @@ export const ScaffolderPageContents = () => { ); const matchesQuery = (metadata: EntityMeta, query: string) => - `${metadata.title}`.toUpperCase().indexOf(query) !== -1 || + `${metadata.title}`.toUpperCase().includes(query) || metadata.tags?.join('').toUpperCase().indexOf(query) !== -1; useEffect(() => { diff --git a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx index ba99a0294a..9e11525347 100644 --- a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx @@ -145,6 +145,7 @@ function useProvideEntityFilters(): FilterGroupsContext { setGroupSelectedFilters, setSelectedCategories, reload, + selectedCategories: selectedCategories.current, loading: !error && !entities, error, filterGroupStates, diff --git a/plugins/scaffolder/src/filter/context.ts b/plugins/scaffolder/src/filter/context.ts index f0a9d3755e..a7819be752 100644 --- a/plugins/scaffolder/src/filter/context.ts +++ b/plugins/scaffolder/src/filter/context.ts @@ -28,6 +28,7 @@ export type FilterGroupsContext = { setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; setSelectedCategories: (categories: string[]) => void; reload: () => Promise; + selectedCategories: string[]; loading: boolean; error?: Error; filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; diff --git a/plugins/scaffolder/src/hooks/useStarredEntities.ts b/plugins/scaffolder/src/hooks/useStarredEntities.ts deleted file mode 100644 index 7cbbbb7ce6..0000000000 --- a/plugins/scaffolder/src/hooks/useStarredEntities.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 { Entity } from '@backstage/catalog-model'; -import { storageApiRef, useApi } from '@backstage/core'; -import { useCallback, useEffect, useState } from 'react'; -import { useObservable } from 'react-use'; - -const buildEntityKey = (component: Entity) => - `entity:${component.kind}:${component.metadata.namespace ?? 'default'}:${ - component.metadata.name - }`; - -export const useStarredEntities = () => { - const storageApi = useApi(storageApiRef); - const settingsStore = storageApi.forBucket('settings'); - const rawStarredEntityKeys = - settingsStore.get('starredEntities') ?? []; - - const [starredEntities, setStarredEntities] = useState( - new Set(rawStarredEntityKeys), - ); - - const observedItems = useObservable( - settingsStore.observe$('starredEntities'), - ); - - useEffect(() => { - if (observedItems?.newValue) { - const currentValue = observedItems?.newValue ?? []; - setStarredEntities(new Set(currentValue)); - } - }, [observedItems?.newValue]); - - const toggleStarredEntity = useCallback( - (entity: Entity) => { - const entityKey = buildEntityKey(entity); - if (starredEntities.has(entityKey)) { - starredEntities.delete(entityKey); - } else { - starredEntities.add(entityKey); - } - - settingsStore.set('starredEntities', Array.from(starredEntities)); - }, - [starredEntities, settingsStore], - ); - - const isStarredEntity = useCallback( - (entity: Entity) => { - const entityKey = buildEntityKey(entity); - return starredEntities.has(entityKey); - }, - [starredEntities], - ); - - return { - starredEntities, - toggleStarredEntity, - isStarredEntity, - }; -};