Enable Filtering of the Catalog Page (#1143)
* feat(catalog/Filters): Add ability to render a React component as the count. Good for loading states and async data rendering for counts. * chore(Catalog/filters): Updating the table so that we use loading states of the table rather than the panel * chore(Catalog/filters): added some nice things for enabling the filtering with some nice count componeents * feat(Catalog/filters): Fetch the correct data and added in enum types to make some nice resolvers * chore(Catalog/filters): Use the new enum type here * chore(Catalog/filters): Removing the unused import to fix lintig * chore(Catalog/filters): Addressing some PR comments * feat(catalog/filters): Making WebStorage return the same instance for the same bucket for subscriptions * chore(core/Storage): fixing some issues with different instances of the storage and adding tests for it * chore(catalog/filters): fixing some tests and trying to remove some of the act warnings in the tests
This commit is contained in:
committed by
Nikita Nek Dudnik
parent
32486d1bd8
commit
32d91af6ce
@@ -42,11 +42,13 @@
|
||||
"@backstage/test-utils": "^0.1.1-alpha.6",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"react-test-renderer": "^16.13.1"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*.{js,d.ts}"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { 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<{}> = () => {
|
||||
const theme = useTheme();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value, loading } = useAsync(() => catalogApi.getEntities());
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress size={theme.spacing(2)} />;
|
||||
}
|
||||
|
||||
return <span>{value?.length ?? '-'}</span>;
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter';
|
||||
import { FilterGroupItem } from '../../types';
|
||||
|
||||
describe('Catalog Filter', () => {
|
||||
it('should render the different groups', async () => {
|
||||
@@ -40,11 +41,11 @@ describe('Catalog Filter', () => {
|
||||
name: 'Test Group 1',
|
||||
items: [
|
||||
{
|
||||
id: 'first',
|
||||
id: FilterGroupItem.ALL,
|
||||
label: 'First Label',
|
||||
},
|
||||
{
|
||||
id: 'second',
|
||||
id: FilterGroupItem.STARRED,
|
||||
label: 'Second Label',
|
||||
},
|
||||
],
|
||||
@@ -67,12 +68,12 @@ describe('Catalog Filter', () => {
|
||||
name: 'Test Group 1',
|
||||
items: [
|
||||
{
|
||||
id: 'first',
|
||||
id: FilterGroupItem.ALL,
|
||||
label: 'First Label',
|
||||
count: 100,
|
||||
},
|
||||
{
|
||||
id: 'second',
|
||||
id: FilterGroupItem.STARRED,
|
||||
label: 'Second Label',
|
||||
count: 400,
|
||||
},
|
||||
@@ -96,12 +97,12 @@ describe('Catalog Filter', () => {
|
||||
name: 'Test Group 1',
|
||||
items: [
|
||||
{
|
||||
id: 'first',
|
||||
id: FilterGroupItem.ALL,
|
||||
label: 'First Label',
|
||||
count: 100,
|
||||
},
|
||||
{
|
||||
id: 'second',
|
||||
id: FilterGroupItem.STARRED,
|
||||
label: 'Second Label',
|
||||
count: 400,
|
||||
},
|
||||
@@ -128,4 +129,29 @@ describe('Catalog Filter', () => {
|
||||
|
||||
expect(onSelectedChangeHandler).toHaveBeenCalledWith(item);
|
||||
});
|
||||
|
||||
it('should render a component when a function is passed to the count component', async () => {
|
||||
const mockGroups: CatalogFilterGroup[] = [
|
||||
{
|
||||
name: 'Test Group 1',
|
||||
items: [
|
||||
{
|
||||
id: FilterGroupItem.ALL,
|
||||
label: 'First Label',
|
||||
count: () => <b>BACKSTAGE!</b>,
|
||||
},
|
||||
{
|
||||
id: FilterGroupItem.STARRED,
|
||||
label: 'Second Label',
|
||||
count: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const { findByText } = render(
|
||||
wrapInTestApp(<CatalogFilter groups={mockGroups} />),
|
||||
);
|
||||
|
||||
expect(await findByText('BACKSTAGE!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,13 +25,12 @@ import {
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import type { IconComponent } from '@backstage/core';
|
||||
|
||||
import { FilterGroupItem } from '../../types';
|
||||
export type CatalogFilterItem = {
|
||||
id: string;
|
||||
id: FilterGroupItem;
|
||||
label: string;
|
||||
icon?: IconComponent;
|
||||
count?: number;
|
||||
loading?: boolean;
|
||||
count?: number | React.FC;
|
||||
};
|
||||
|
||||
export type CatalogFilterGroup = {
|
||||
@@ -105,7 +104,11 @@ export const CatalogFilter: React.FC<CatalogFilterProps> = ({
|
||||
{item.label}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
{item.count}
|
||||
{typeof item.count === 'function' ? (
|
||||
<item.count />
|
||||
) : (
|
||||
item.count
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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']),
|
||||
});
|
||||
|
||||
const { findByText } = render(wrapInTestApp(<StarredCount />));
|
||||
|
||||
expect(await findByText('4')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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>;
|
||||
};
|
||||
@@ -18,47 +18,34 @@ import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import CatalogPage from './CatalogPage';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
CreateStorageApiOptions,
|
||||
ApiProvider,
|
||||
errorApiRef,
|
||||
storageApiRef,
|
||||
StorageApi,
|
||||
WebStorage,
|
||||
} from '@backstage/core';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { wrapInTestApp, MockErrorApi } from '@backstage/test-utils';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { CatalogApi } from '../../api/types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const errorApi = { post: () => {} };
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
metadata: {
|
||||
name: 'Entity1',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
},
|
||||
] as Entity[]),
|
||||
getLocationByEntity: () =>
|
||||
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
|
||||
};
|
||||
|
||||
const mockWebStorageErrorApi = { post: jest.fn(), error$: jest.fn() };
|
||||
const createWebStorage = (
|
||||
args?: Partial<CreateStorageApiOptions>,
|
||||
): StorageApi => {
|
||||
return WebStorage.create({
|
||||
errorApi: mockWebStorageErrorApi,
|
||||
...args,
|
||||
});
|
||||
};
|
||||
const storageApi = createWebStorage();
|
||||
|
||||
describe('CatalogPage', () => {
|
||||
const mockErrorApi = new MockErrorApi();
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
metadata: {
|
||||
name: 'Entity1',
|
||||
},
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
},
|
||||
] as Entity[]),
|
||||
getLocationByEntity: () =>
|
||||
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -67,9 +54,9 @@ describe('CatalogPage', () => {
|
||||
wrapInTestApp(
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.from([
|
||||
[errorApiRef, errorApi],
|
||||
[errorApiRef, mockErrorApi],
|
||||
[catalogApiRef, catalogApi],
|
||||
[storageApiRef, storageApi],
|
||||
[storageApiRef, new WebStorage('@mock', mockErrorApi)],
|
||||
])}
|
||||
>
|
||||
<CatalogPage />
|
||||
|
||||
@@ -27,19 +27,22 @@ import {
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
|
||||
import { Button, Link, makeStyles, Typography } from '@material-ui/core';
|
||||
import { Button, makeStyles, Typography, Link } from '@material-ui/core';
|
||||
import GitHub from '@material-ui/icons/GitHub';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { Component } from '../../data/component';
|
||||
import { defaultFilter, filterGroups } from '../../data/filters';
|
||||
import { defaultFilter, filterGroups, dataResolvers } from '../../data/filters';
|
||||
import { entityToComponent, findLocationForEntityMeta } from '../../data/utils';
|
||||
import {
|
||||
CatalogFilter,
|
||||
CatalogFilterItem,
|
||||
} from '../CatalogFilter/CatalogFilter';
|
||||
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
|
||||
import CatalogTable from '../CatalogTable/CatalogTable';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -57,15 +60,20 @@ const useStyles = makeStyles(theme => ({
|
||||
|
||||
const CatalogPage: FC<{}> = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value, error, loading } = useAsync(() => catalogApi.getEntities());
|
||||
const { starredEntities } = useStarredEntities();
|
||||
const [selectedFilter, setSelectedFilter] = useState<CatalogFilterItem>(
|
||||
defaultFilter,
|
||||
);
|
||||
const { value, error, loading } = useAsync(
|
||||
() => dataResolvers[selectedFilter.id]({ catalogApi, starredEntities }),
|
||||
[selectedFilter.id],
|
||||
);
|
||||
|
||||
const onFilterSelected = useCallback(
|
||||
selected => setSelectedFilter(selected),
|
||||
[],
|
||||
);
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
const actions = [
|
||||
|
||||
@@ -41,16 +41,6 @@ const components: Component[] = [
|
||||
];
|
||||
|
||||
describe('CatalogTable component', () => {
|
||||
it('should render loading when loading prop it set to true', async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<CatalogTable titlePreamble="Owned" components={[]} loading />,
|
||||
),
|
||||
);
|
||||
const progress = await rendered.findByTestId('progress');
|
||||
expect(progress).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message when error is passed in props', async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Progress, Table, TableColumn } from '@backstage/core';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { FC } from 'react';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { Component } from '../../data/component';
|
||||
|
||||
import { entityRoute } from '../../routes';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
@@ -60,9 +61,7 @@ const CatalogTable: FC<CatalogTableProps> = ({
|
||||
titlePreamble,
|
||||
actions,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert severity="error">
|
||||
@@ -74,8 +73,14 @@ const CatalogTable: FC<CatalogTableProps> = ({
|
||||
|
||||
return (
|
||||
<Table
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{ paging: false, actionsColumnIndex: -1 }}
|
||||
options={{
|
||||
paging: false,
|
||||
actionsColumnIndex: -1,
|
||||
loadingType: 'linear',
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title={`${titlePreamble} (${(components && components.length) || 0})`}
|
||||
data={components}
|
||||
actions={actions}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('ComponentContextMenu', () => {
|
||||
it('should call onUnregisterComponent on button click', async () => {
|
||||
await act(async () => {
|
||||
const mockCallback = jest.fn();
|
||||
const menu = await render(
|
||||
const menu = render(
|
||||
<ComponentContextMenu onUnregisterComponent={mockCallback} />,
|
||||
);
|
||||
const button = await menu.findByTestId('menu-button');
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import ComponentPage from './ComponentPage';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, wait } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
|
||||
@@ -38,7 +38,7 @@ const errorApi = { post: () => {} };
|
||||
describe('ComponentPage', () => {
|
||||
it('should redirect to component table page when name is not provided', async () => {
|
||||
const props = getTestProps('');
|
||||
await render(
|
||||
render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.from([
|
||||
@@ -55,6 +55,9 @@ describe('ComponentPage', () => {
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(props.history.push).toHaveBeenCalledWith('/catalog');
|
||||
|
||||
await wait(() =>
|
||||
expect(props.history.push).toHaveBeenCalledWith('/catalog'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,21 +19,26 @@ import {
|
||||
} from '../components/CatalogFilter/CatalogFilter';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import { StarredCount } from '../components/CatalogFilter/StarredCount';
|
||||
import { AllServicesCount } from '../components/CatalogFilter/AllServicesCount';
|
||||
import { FilterGroupItem } from '../types';
|
||||
import { CatalogApi } from '../..';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const filterGroups: CatalogFilterGroup[] = [
|
||||
{
|
||||
name: 'Personal',
|
||||
items: [
|
||||
{
|
||||
id: 'owned',
|
||||
id: FilterGroupItem.OWNED,
|
||||
label: 'Owned',
|
||||
count: 123,
|
||||
count: 0,
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
id: 'starred',
|
||||
id: FilterGroupItem.STARRED,
|
||||
label: 'Starred',
|
||||
count: 10,
|
||||
count: StarredCount,
|
||||
icon: StarIcon,
|
||||
},
|
||||
],
|
||||
@@ -43,12 +48,34 @@ export const filterGroups: CatalogFilterGroup[] = [
|
||||
name: 'Company',
|
||||
items: [
|
||||
{
|
||||
id: 'all',
|
||||
id: FilterGroupItem.ALL,
|
||||
label: 'All Services',
|
||||
count: 123,
|
||||
count: AllServicesCount,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
type ResolverFunction = ({
|
||||
catalogApi,
|
||||
starredEntities,
|
||||
}: {
|
||||
catalogApi: CatalogApi;
|
||||
starredEntities: Set<string>;
|
||||
}) => Promise<Entity[]>;
|
||||
|
||||
export const dataResolvers: Record<FilterGroupItem, ResolverFunction> = {
|
||||
[FilterGroupItem.OWNED]: async () => [],
|
||||
[FilterGroupItem.ALL]: async ({ catalogApi }) => {
|
||||
return catalogApi.getEntities();
|
||||
},
|
||||
[FilterGroupItem.STARRED]: async ({ catalogApi, starredEntities }) => {
|
||||
const allEntities = await catalogApi.getEntities();
|
||||
|
||||
return allEntities.filter(entity =>
|
||||
starredEntities.has(entity.metadata.name),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0];
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { useApi, storageApiRef } from '@backstage/core';
|
||||
import { useObservable } from 'react-use';
|
||||
|
||||
export const useStarredEntities = () => {
|
||||
const storageApi = useApi(storageApiRef);
|
||||
const settingsStore = storageApi.forBucket('settings');
|
||||
const rawStarredItems = settingsStore.get<string[]>('starredEntities') ?? [];
|
||||
|
||||
const [starredEntities, setStarredEntities] = useState(
|
||||
new Set(rawStarredItems),
|
||||
);
|
||||
|
||||
const observedItems = useObservable(
|
||||
settingsStore.observe$<string[]>('starredEntities'),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (observedItems?.newValue) {
|
||||
const currentValue = observedItems?.newValue ?? [];
|
||||
setStarredEntities(new Set(currentValue));
|
||||
}
|
||||
}, [observedItems?.newValue]);
|
||||
|
||||
return {
|
||||
starredEntities,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { useStarredEntities } from './useStarredEntites';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
storageApiRef,
|
||||
WebStorage,
|
||||
StorageApi,
|
||||
} from '@backstage/core';
|
||||
import { MockErrorApi } from '@backstage/test-utils';
|
||||
|
||||
describe('useStarredEntities', () => {
|
||||
let mockStorage: StorageApi | undefined;
|
||||
|
||||
const wrapper: React.FC<{}> = ({ children }) => {
|
||||
return (
|
||||
<ApiProvider apis={ApiRegistry.with(storageApiRef, mockStorage)}>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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 store = mockStorage?.forBucket('settings');
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useStarredEntities(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.starredEntities.size).toBe(0);
|
||||
expect(result.current.starredEntities.has('something')).toBeFalsy();
|
||||
|
||||
// Make this happen after awaiting for the next update so we can
|
||||
// catch when the hook re-renders with the latest data
|
||||
setTimeout(() => store?.set('starredEntities', ['something']), 1);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.starredEntities.size).toBe(1);
|
||||
expect(result.current.starredEntities.has('something')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -157,3 +157,9 @@ export type KindParser = {
|
||||
envelope: DescriptorEnvelope,
|
||||
): Promise<DescriptorEnvelope | undefined>;
|
||||
};
|
||||
|
||||
export enum FilterGroupItem {
|
||||
ALL = 'ALL',
|
||||
STARRED = 'STARRED',
|
||||
OWNED = 'OWNED',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user