Merge pull request #1349 from spotify/ndudnik/filter-by-identity

Filter by identity
This commit is contained in:
Nikita Dudnik
2020-06-22 09:50:12 +02:00
committed by GitHub
11 changed files with 354 additions and 200 deletions
+16 -4
View File
@@ -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?
+1 -1
View File
@@ -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();
@@ -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 <CircularProgress size={theme.spacing(2)} />;
}
return <span>{value?.length ?? '-'}</span>;
return <span>{value ?? length ?? '-'}</span>;
};
@@ -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(<CatalogFilter groups={mockGroups} />),
wrapInTestApp(
<CatalogFilter {...defaultFilterProps} groups={mockGroups} />,
),
);
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(<CatalogFilter groups={mockGroups} />),
wrapInTestApp(
<CatalogFilter {...defaultFilterProps} groups={mockGroups} />,
),
);
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(<CatalogFilter groups={mockGroups} />),
const { getAllByText } = render(
wrapInTestApp(
<CatalogFilter {...defaultFilterProps} groups={mockGroups} />,
),
);
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(
<CatalogFilter
{...defaultFilterProps}
groups={mockGroups}
onSelectedChange={onSelectedChangeHandler}
onFilterChange={onSelectedChangeHandler}
/>,
),
);
@@ -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: () => <b>BACKSTAGE!</b>,
},
{
id: EntityFilterType.STARRED,
id: EntityGroup.STARRED,
label: 'Second Label',
count: 400,
},
@@ -149,9 +198,11 @@ describe('Catalog Filter', () => {
},
];
const { findByText } = render(
wrapInTestApp(<CatalogFilter groups={mockGroups} />),
wrapInTestApp(
<CatalogFilter {...defaultFilterProps} groups={mockGroups} />,
),
);
expect(await findByText('BACKSTAGE!')).toBeInTheDocument();
expect(await findByText('Test Group 1')).toBeInTheDocument();
});
});
@@ -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>(theme => ({
root: {
backgroundColor: 'rgba(0, 0, 0, .11)',
@@ -71,10 +67,16 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
export const CatalogFilter: React.FC<CatalogFilterProps> = ({
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<CatalogFilterProps> = ({
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<CatalogFilterProps> = ({
{item.label}
</Typography>
</ListItemText>
{typeof item.count === 'function' ? (
<item.count />
) : (
item.count
)}
{entitiesByFilter[item.id]?.length ?? '-'}
</MenuItem>
))}
</List>
@@ -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(<StarredCount />));
expect(await findByText('4')).toBeInTheDocument();
});
});
@@ -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 <span>{starredEntities.size}</span>;
};
@@ -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<IdentityApi> = {
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(
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, mockErrorApi],
[catalogApiRef, catalogApi],
[storageApiRef, new WebStorage('@mock', mockErrorApi)],
[identityApiRef, mockIndentityApi],
])}
>
<CatalogPage />
</ApiProvider>,
),
);
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(
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, mockErrorApi],
[catalogApiRef, catalogApi],
[storageApiRef, new WebStorage('@mock', mockErrorApi)],
[identityApiRef, mockIndentityApi],
])}
>
<CatalogPage />
</ApiProvider>,
),
);
fireEvent.click(getByText(/Owned/));
const items = await findByText(/Owned \(1\)/);
expect(items).toBeInTheDocument();
});
});
@@ -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<string>(tabs[0].id);
const { toggleStarredEntity, isStarredEntity } = useStarredEntities();
const [selectedFilter, setSelectedFilter] = useState<CatalogFilterItem>(
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 (
<CatalogLayout>
<HeaderTabs
tabs={tabs}
onChange={index => {
setSelectedTab(tabs[index as number].id);
tabs={labeledEntityTypes}
onChange={(index: Number) => {
selectTypeFilter(labeledEntityTypes[index as number].id);
}}
/>
<Content>
@@ -212,14 +170,18 @@ export const CatalogPage: FC<{}> = () => {
<div>
<CatalogFilter
groups={filterGroups}
selectedId={selectedFilter.id}
onSelectedChange={onFilterSelected}
selectedFilter={selectedFilter ?? EntityGroup.ALL}
onFilterChange={setSelectedFilter}
entitiesByFilter={entitiesByFilter}
/>
</div>
<CatalogTable
titlePreamble={selectedFilter.label}
titlePreamble={
getCatalogFilterItemByType(selectedFilter ?? EntityGroup.ALL)
?.label ?? ''
}
entities={filteredEntities || []}
loading={!entities && !error}
loading={loading && !error}
error={error}
actions={actions}
/>
+62 -18
View File
@@ -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<string, EntityFilter> = {
[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];
+103
View File
@@ -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<EntityGroup, Entity[] | undefined>;
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<string>(
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,
};
};