diff --git a/docs/FAQ.md b/docs/FAQ.md index feb1667717..85c8f09bef 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -11,12 +11,16 @@ brand. ### Is Backstage a monitoring platform? -No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into Backstage by writing [a plugin](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage). +No, but it can be! Backstage is designed to be a developer portal for all your +infrastructure tooling, services, and documentation. So, it's not a monitoring +platform — but that doesn't mean you can't integrate a monitoring tool into +Backstage by writing +[a plugin](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage). ### How is Backstage licensed? -Backstage was released as open sourced software by Spotify and is licensed -under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). +Backstage was released as open sourced software by Spotify and is licensed under +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). ### Why did we open source Backstage? @@ -95,7 +99,15 @@ plugins. ​ ### What is a "plugin" in Backstage? -By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage)" above) +By far, our most-used plugin is our TechDocs plugin, which we use for creating +technical documentation. Our philosophy at Spotify is to treat "docs like code", +where you write documentation using the same workflow as you write your code. +This makes it easier to create, find, and update documentation. We hope to +release +[the open source version](https://github.com/spotify/backstage/issues/687) in +the future. (See also: +"[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage)" +above) ### Do I have to write plugins in TypeScript? diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 0450f3969d..65b2ec5aaa 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -2,7 +2,6 @@ import { ApiRegistry, alertApiRef, errorApiRef, - identityApiRef, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, @@ -12,6 +11,7 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, + identityApiRef, } from '@backstage/core'; const builder = ApiRegistry.builder(); diff --git a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx index 5ccac8f772..b4d90c249a 100644 --- a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx +++ b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx @@ -14,13 +14,13 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import { useApi } from '@backstage/core'; import { catalogApiRef } from '../../api/types'; import { useAsync } from 'react-use'; import { CircularProgress, useTheme } from '@material-ui/core'; -export const AllServicesCount: React.FC<{}> = () => { +export const AllServicesCount: FC<{}> = () => { const theme = useTheme(); const catalogApi = useApi(catalogApiRef); const { value, loading } = useAsync(() => catalogApi.getEntities()); @@ -29,5 +29,5 @@ export const AllServicesCount: React.FC<{}> = () => { return ; } - return {value?.length ?? '-'}; + return {value ?? length ?? '-'}; }; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index b78450d549..cc4cc62fdb 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -18,16 +18,57 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; -import { EntityFilterType } from '../../data/filters'; +import { EntityGroup } from '../../data/filters'; describe('Catalog Filter', () => { + const comp1 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component-1', + }, + spec: { + owner: 'team', + }, + }; + const comp2 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component-2', + }, + spec: { + owner: 'team', + }, + }; + const comp3 = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component-3', + }, + spec: { + owner: '', + }, + }; + const defaultFilterProps = { + selectedFilter: EntityGroup.ALL, + onFilterChange: (type: EntityGroup) => type, + entitiesByFilter: { + [EntityGroup.ALL]: [comp1, comp2, comp3], + [EntityGroup.STARRED]: [comp1], + [EntityGroup.OWNED]: [comp1], + }, + }; it('should render the different groups', async () => { const mockGroups: CatalogFilterGroup[] = [ { name: 'Test Group 1', items: [] }, { name: 'Test Group 2', items: [] }, ]; const { findByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); for (const group of mockGroups) { @@ -41,11 +82,11 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', }, ], @@ -53,7 +94,9 @@ describe('Catalog Filter', () => { ]; const { findByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); const [group] = mockGroups; @@ -68,26 +111,31 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', - count: 100, + count: 3, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', - count: 400, + count: 1, }, ], }, ]; - const { findByText } = render( - wrapInTestApp(), + const { getAllByText } = render( + wrapInTestApp( + , + ), ); - const [group] = mockGroups; - for (const item of group.items) { - expect(await findByText(item.count!.toString())).toBeInTheDocument(); + for (const key of Object.keys(defaultFilterProps.entitiesByFilter)) { + const matcher = new RegExp( + `(${defaultFilterProps.entitiesByFilter[key as EntityGroup].length})`, + ); + const items = await getAllByText(matcher); + items.forEach(el => expect(el).toBeInTheDocument()); } }); @@ -97,12 +145,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', count: 100, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', count: 400, }, @@ -115,8 +163,9 @@ describe('Catalog Filter', () => { const { findByText } = render( wrapInTestApp( , ), ); @@ -127,7 +176,7 @@ describe('Catalog Filter', () => { fireEvent.click(element); - expect(onSelectedChangeHandler).toHaveBeenCalledWith(item); + expect(onSelectedChangeHandler).toHaveBeenCalledWith(item.id); }); it('should render a component when a function is passed to the count component', async () => { @@ -136,12 +185,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: EntityFilterType.ALL, + id: EntityGroup.ALL, label: 'First Label', count: () => BACKSTAGE!, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Second Label', count: 400, }, @@ -149,9 +198,11 @@ describe('Catalog Filter', () => { }, ]; const { findByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); - expect(await findByText('BACKSTAGE!')).toBeInTheDocument(); + expect(await findByText('Test Group 1')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 5335b33ffa..61a83fccd8 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import { Card, List, @@ -26,12 +26,14 @@ import { makeStyles, } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; -import { EntityFilterType } from '../../data/filters'; +import { EntityGroup } from '../../data/filters'; +import { EntitiesByFilter } from '../../hooks/useEntities'; + export type CatalogFilterItem = { - id: EntityFilterType; + id: EntityGroup; label: string; icon?: IconComponent; - count?: number | React.FC; + count?: number | FC; }; export type CatalogFilterGroup = { @@ -39,12 +41,6 @@ export type CatalogFilterGroup = { items: CatalogFilterItem[]; }; -export type CatalogFilterProps = { - groups: CatalogFilterGroup[]; - selectedId?: string; - onSelectedChange?: (item: CatalogFilterItem) => void; -}; - const useStyles = makeStyles(theme => ({ root: { backgroundColor: 'rgba(0, 0, 0, .11)', @@ -71,10 +67,16 @@ const useStyles = makeStyles(theme => ({ }, })); -export const CatalogFilter: React.FC = ({ +export const CatalogFilter: FC<{ + selectedFilter: EntityGroup; + onFilterChange: (type: EntityGroup) => void; + entitiesByFilter: EntitiesByFilter; + groups: CatalogFilterGroup[]; +}> = ({ + selectedFilter: selectedId, + onFilterChange: setSelectedFilter, + entitiesByFilter, groups, - selectedId, - onSelectedChange, }) => { const classes = useStyles(); return ( @@ -91,7 +93,9 @@ export const CatalogFilter: React.FC = ({ key={item.id} button divider - onClick={() => onSelectedChange?.(item)} + onClick={() => { + setSelectedFilter(item.id); + }} selected={item.id === selectedId} className={classes.menuItem} > @@ -105,11 +109,7 @@ export const CatalogFilter: React.FC = ({ {item.label} - {typeof item.count === 'function' ? ( - - ) : ( - item.count - )} + {entitiesByFilter[item.id]?.length ?? '-'} ))} diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx deleted file mode 100644 index 546d00e854..0000000000 --- a/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx +++ /dev/null @@ -1,35 +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 from 'react'; -import { render } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { StarredCount } from './StarredCount'; -import * as Hooks from '../../hooks/useStarredEntites'; - -describe('Starred Count', () => { - it('should render the count returned from the hook', async () => { - jest.spyOn(Hooks, 'useStarredEntities').mockReturnValue({ - starredEntities: new Set(['id1', 'id2', 'id3', 'id4']), - isStarredEntity: () => false, - toggleStarredEntity: () => undefined, - }); - - const { findByText } = render(wrapInTestApp()); - - expect(await findByText('4')).toBeInTheDocument(); - }); -}); diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx deleted file mode 100644 index 5e79682783..0000000000 --- a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx +++ /dev/null @@ -1,23 +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 from 'react'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; - -export const StarredCount: React.FC<{}> = () => { - const { starredEntities } = useStarredEntities(); - return {starredEntities.size}; -}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 2e1989323c..7ba1c39bb7 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -20,9 +20,11 @@ import { errorApiRef, storageApiRef, WebStorage, + IdentityApi, + identityApiRef, } from '@backstage/core'; import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import React from 'react'; import { catalogApiRef } from '../..'; import { CatalogApi } from '../../api/types'; @@ -40,31 +42,69 @@ describe('CatalogPage', () => { }, apiVersion: 'backstage.io/v1alpha1', kind: 'Component', + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + { + metadata: { + name: 'Entity2', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + owner: 'not-tools@example.com', + type: 'service', + }, }, ] as Entity[]), getLocationByEntity: () => Promise.resolve({ id: 'id', type: 'github', target: 'url' }), }; + const mockIndentityApi: Partial = { + getUserId: () => 'tools@example.com', + }; // this test right now causes some red lines in the log output when running tests // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { - const rendered = render( + const { findByText } = render( wrapInTestApp( , ), ); - expect( - await rendered.findByText('Backstage Service Catalog'), - ).toBeInTheDocument(); + + const items = await findByText(/All Services \(2\)/); + expect(items).toBeInTheDocument(); + }); + it('should filter by owner', async () => { + const { findByText, getByText } = render( + wrapInTestApp( + + + , + ), + ); + fireEvent.click(getByText(/Owned/)); + const items = await findByText(/Owned \(1\)/); + expect(items).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 8c9313ca66..ef77cf573f 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -21,7 +21,6 @@ import { DismissableBanner, HeaderTabs, SupportButton, - useApi, } from '@backstage/core'; import CatalogLayout from './CatalogLayout'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; @@ -36,47 +35,18 @@ import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import Star from '@material-ui/icons/Star'; import StarOutline from '@material-ui/icons/StarBorder'; -import React, { FC, useCallback, useState, useMemo } from 'react'; +import React, { FC } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { catalogApiRef } from '../..'; -import { - defaultFilter, - entityFilters, - filterGroups, - EntityFilterType, -} from '../../data/filters'; -import { findLocationForEntityMeta } from '../../data/utils'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; -import { - CatalogFilter, - CatalogFilterItem, -} from '../CatalogFilter/CatalogFilter'; +import { CatalogFilter } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; -import useStaleWhileRevalidate from 'swr'; - -// TODO: replace me with the proper tabs implemntation -const tabs = [ - { - id: 'service', - label: 'Services', - }, - { - id: 'website', - label: 'Websites', - }, - { - id: 'lib', - label: 'Libraries', - }, - { - id: 'documentation', - label: 'Documentation', - }, - { - id: 'other', - label: 'Other', - }, -]; +import { useEntities } from '../../hooks/useEntities'; +import { findLocationForEntityMeta } from '../../data/utils'; +import { + getCatalogFilterItemByType, + EntityGroup, + filterGroups, + labeledEntityTypes, +} from '../../data/filters'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -92,30 +62,18 @@ const useStyles = makeStyles(theme => ({ })); export const CatalogPage: FC<{}> = () => { - const catalogApi = useApi(catalogApiRef); - const [selectedTab, setSelectedTab] = useState(tabs[0].id); - const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); - const [selectedFilter, setSelectedFilter] = useState( - defaultFilter, - ); + const { + entitiesByFilter, + error, + loading, + selectedFilter, + setSelectedFilter, + toggleStarredEntity, + isStarredEntity, + selectTypeFilter, + } = useEntities(); - const { data: entities, error } = useStaleWhileRevalidate( - ['catalog/all', entityFilters[selectedFilter.id]], - async () => catalogApi.getEntities(), - ); - - const onFilterSelected = useCallback( - selected => setSelectedFilter(selected), - [], - ); - - const filteredEntities = useMemo(() => { - const typeFilter = entityFilters[EntityFilterType.TYPE]; - const leftMenuFilter = entityFilters[selectedFilter.id]; - return entities - ?.filter(e => leftMenuFilter(e, { isStarred: isStarredEntity(e) })) - .filter(e => typeFilter(e, { type: selectedTab })); - }, [selectedFilter.id, selectedTab, isStarredEntity, entities?.filter]); + const filteredEntities = entitiesByFilter[selectedFilter ?? EntityGroup.ALL]; const styles = useStyles(); @@ -174,9 +132,9 @@ export const CatalogPage: FC<{}> = () => { return ( { - setSelectedTab(tabs[index as number].id); + tabs={labeledEntityTypes} + onChange={(index: Number) => { + selectTypeFilter(labeledEntityTypes[index as number].id); }} /> @@ -212,14 +170,18 @@ export const CatalogPage: FC<{}> = () => {
diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts index bbe959caa3..c13f1a2d9c 100644 --- a/plugins/catalog/src/data/filters.ts +++ b/plugins/catalog/src/data/filters.ts @@ -17,18 +17,15 @@ import { Entity } from '@backstage/catalog-model'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; -import { AllServicesCount } from '../components/CatalogFilter/AllServicesCount'; import { CatalogFilterGroup, CatalogFilterItem, } from '../components/CatalogFilter/CatalogFilter'; -import { StarredCount } from '../components/CatalogFilter/StarredCount'; -export enum EntityFilterType { +export enum EntityGroup { ALL = 'ALL', STARRED = 'STARRED', OWNED = 'OWNED', - TYPE = 'TYPE', } export const filterGroups: CatalogFilterGroup[] = [ @@ -36,15 +33,13 @@ export const filterGroups: CatalogFilterGroup[] = [ name: 'Personal', items: [ { - id: EntityFilterType.OWNED, + id: EntityGroup.OWNED, label: 'Owned', - count: 0, icon: SettingsIcon, }, { - id: EntityFilterType.STARRED, + id: EntityGroup.STARRED, label: 'Starred', - count: StarredCount, icon: StarIcon, }, ], @@ -54,26 +49,75 @@ export const filterGroups: CatalogFilterGroup[] = [ name: 'Company', items: [ { - id: EntityFilterType.ALL, - label: 'All Entities', - count: AllServicesCount, + id: EntityGroup.ALL, + label: 'All Services', }, ], }, ]; +export const getCatalogFilterItemByType = (filterType: EntityGroup) => { + for (const group of filterGroups) { + for (const filter of group.items) { + if (filter.id === filterType) { + return filter; + } + } + } + return null; +}; + type EntityFilter = (entity: Entity, options: EntityFilterOptions) => boolean; -type EntityFilterOptions = { - isStarred?: boolean; - type?: string; +type EntityFilterOptions = Partial<{ + isStarred: boolean; + userId: string; +}>; + +type Owned = { + owner: string; }; export const entityFilters: Record = { - [EntityFilterType.OWNED]: () => false, - [EntityFilterType.ALL]: () => true, - [EntityFilterType.STARRED]: (_, { isStarred }) => !!isStarred, - [EntityFilterType.TYPE]: (e, { type }) => (e.spec as any)?.type === type, + [EntityGroup.OWNED]: (e, { userId }) => { + const owner = (e.spec! as Owned).owner; + return owner === userId; + }, + [EntityGroup.ALL]: () => true, + [EntityGroup.STARRED]: (_, { isStarred }) => !!isStarred, }; +export const entityTypeFilter = (e: Entity, type: string) => + (e.spec as any)?.type === type; + +type EntityType = 'service' | 'website' | 'lib' | 'documentation' | 'other'; + +type LabeledEntityType = { + id: EntityType; + label: string; +}; + +export const labeledEntityTypes: LabeledEntityType[] = [ + { + id: 'service', + label: 'Services', + }, + { + id: 'website', + label: 'Websites', + }, + { + id: 'lib', + label: 'Libraries', + }, + { + id: 'documentation', + label: 'Documentation', + }, + { + id: 'other', + label: 'Other', + }, +]; + export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0]; diff --git a/plugins/catalog/src/hooks/useEntities.ts b/plugins/catalog/src/hooks/useEntities.ts new file mode 100644 index 0000000000..6509bd9b4b --- /dev/null +++ b/plugins/catalog/src/hooks/useEntities.ts @@ -0,0 +1,103 @@ +/* + * 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 { useState, useMemo } from 'react'; +import { + EntityGroup, + entityFilters, + entityTypeFilter, + labeledEntityTypes, +} from '../data/filters'; +import { useApi, identityApiRef } from '@backstage/core'; +import { catalogApiRef } from '..'; +import { useStarredEntities } from './useStarredEntites'; +import { Entity } from '@backstage/catalog-model'; +import useStaleWhileRevalidate from 'swr'; + +export type EntitiesByFilter = Record; + +type UseEntities = { + selectedFilter: EntityGroup | undefined; + setSelectedFilter: (f: EntityGroup) => void; + error: Error | null; + toggleStarredEntity: any; + isStarredEntity: (e: Entity) => boolean; + entitiesByFilter: EntitiesByFilter; + loading: boolean; + selectedTypeFilter: string; + selectTypeFilter: (id: string) => void; +}; + +export const useEntities = (): UseEntities => { + const [selectedFilter, setSelectedFilter] = useState< + EntityGroup | undefined + >(); + const catalogApi = useApi(catalogApiRef); + const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); + const { data: entities, error } = useStaleWhileRevalidate( + ['catalog/all', entityFilters[selectedFilter ?? EntityGroup.ALL]], + async () => catalogApi.getEntities(), + ); + + const indentityApi = useApi(identityApiRef); + const userId = indentityApi.getUserId(); + + const [selectedTypeFilter, selectTypeFilter] = useState( + labeledEntityTypes[0].id, + ); + + const entitiesByFilter = useMemo(() => { + const filterEntities = ( + ents: Entity[] | undefined, + filterId: EntityGroup, + isStarred: (e: Entity) => boolean, + user: string, + ) => { + return ents + ?.filter((e: Entity) => + entityFilters[filterId](e, { + isStarred: isStarred(e), + userId: user, + }), + ) + .filter(e => entityTypeFilter(e, selectedTypeFilter)); + }; + const data = Object.keys(EntityGroup).reduce( + (res, key) => ({ + ...res, + [key]: filterEntities( + entities, + key as EntityGroup, + isStarredEntity, + userId, + ), + }), + {} as EntitiesByFilter, + ); + return data; + }, [entities, isStarredEntity, userId, selectedTypeFilter]); + + return { + selectedFilter, + setSelectedFilter, + error, + toggleStarredEntity, + isStarredEntity, + entitiesByFilter, + loading: entities === undefined, + selectedTypeFilter, + selectTypeFilter, + }; +};