plugin-techdocs: add group filtering support to EntityListDocsGrid
Signed-off-by: Simon Ninon <sninon@pagerduty.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 { ApiProvider, ConfigReader } from '@backstage/core-app-api';
|
||||
import {
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
starredEntitiesApiRef,
|
||||
MockEntityListContextProvider,
|
||||
MockStarredEntitiesApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
MockStorageApi,
|
||||
renderInTestApp,
|
||||
TestApiRegistry,
|
||||
} from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { rootDocsRouteRef } from '../../../routes';
|
||||
import { EntityListDocsGrid } from './EntityListDocsGrid';
|
||||
|
||||
const entities = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'Documentation #1',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'documentation',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'Documentation #2',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'documentation',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockCatalogApi = {
|
||||
getEntityByRef: () => Promise.resolve(),
|
||||
getEntities: async () => ({
|
||||
items: entities,
|
||||
}),
|
||||
} as Partial<CatalogApi>;
|
||||
|
||||
describe('Entity List Docs Grid', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const configApi: ConfigApi = new ConfigReader({
|
||||
organization: {
|
||||
name: 'My Company',
|
||||
},
|
||||
});
|
||||
|
||||
const storageApi = MockStorageApi.create();
|
||||
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[configApiRef, configApi],
|
||||
[storageApiRef, storageApi],
|
||||
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
|
||||
);
|
||||
|
||||
it('should render all entitites without filtering', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: entities }}>
|
||||
<EntityListDocsGrid />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.queryByText('All Documentation')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Documentation #1')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Documentation #2')).toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render only filtered entities with filtering', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: entities }}>
|
||||
<EntityListDocsGrid
|
||||
groups={[
|
||||
{
|
||||
title: 'Curated Documentation',
|
||||
filterPredicate: entity =>
|
||||
entity.metadata.name === 'Documentation #1',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.queryByText('Curated Documentation'),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Documentation #1')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #2'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render nothing with filtering yielding no result', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: entities }}>
|
||||
<EntityListDocsGrid
|
||||
groups={[
|
||||
{
|
||||
title: 'Curated Documentation',
|
||||
filterPredicate: entity =>
|
||||
entity.metadata.name === 'Documentation #3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.queryByText('Curated Documentation'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #1'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #2'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an error without any documentation and without filtering', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: [] }}>
|
||||
<EntityListDocsGrid />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.queryByText('All Documentation'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #1'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #2'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -15,20 +15,95 @@
|
||||
*/
|
||||
|
||||
import { DocsCardGrid } from './DocsCardGrid';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
Content,
|
||||
ContentHeader,
|
||||
Link,
|
||||
Progress,
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntityList } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
useEntityList,
|
||||
useEntityOwnership,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Props for {@link EntityListDocsGrid}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type DocsGroupConfig = {
|
||||
title: React.ReactNode;
|
||||
filterPredicate: ((entity: Entity) => boolean) | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link EntityListDocsGrid}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityListDocsGridPageProps = {
|
||||
groups?: DocsGroupConfig[];
|
||||
};
|
||||
|
||||
const allEntitiesGroup: DocsGroupConfig = {
|
||||
title: 'All Documentation',
|
||||
filterPredicate: () => true,
|
||||
};
|
||||
|
||||
const EntityListDocsGridGroup = ({
|
||||
entities,
|
||||
group,
|
||||
}: {
|
||||
group: DocsGroupConfig;
|
||||
entities: Entity[];
|
||||
}) => {
|
||||
const { loading: loadingOwnership, isOwnedEntity } = useEntityOwnership();
|
||||
|
||||
const shownEntities = entities.filter(entity => {
|
||||
if (group.filterPredicate === 'ownedByUser') {
|
||||
if (loadingOwnership) {
|
||||
return false;
|
||||
}
|
||||
return isOwnedEntity(entity);
|
||||
}
|
||||
|
||||
return (
|
||||
typeof group.filterPredicate === 'function' &&
|
||||
group.filterPredicate(entity)
|
||||
);
|
||||
});
|
||||
|
||||
const titleComponent: React.ReactNode = (() => {
|
||||
return typeof group.title === 'string' ? (
|
||||
<ContentHeader title={group.title} />
|
||||
) : (
|
||||
group.title
|
||||
);
|
||||
})();
|
||||
|
||||
if (shownEntities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
{titleComponent}
|
||||
<DocsCardGrid entities={shownEntities} />
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component responsible to get entities from entity list context and pass down to DocsCardGrid
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityListDocsGrid = () => {
|
||||
export const EntityListDocsGrid = ({ groups }: EntityListDocsGridPageProps) => {
|
||||
const { loading, error, entities } = useEntityList();
|
||||
|
||||
if (error) {
|
||||
@@ -42,15 +117,39 @@ export const EntityListDocsGrid = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !entities) {
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (entities.length === 0) {
|
||||
return (
|
||||
<div data-testid="doc-not-found">
|
||||
<Typography variant="body2">
|
||||
No documentation found that match your filter. Learn more about{' '}
|
||||
<Link to="https://backstage.io/docs/features/techdocs/creating-and-publishing">
|
||||
publishing documentation
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
entities.sort((a, b) =>
|
||||
(a.metadata.title ?? a.metadata.name).localeCompare(
|
||||
b.metadata.title ?? b.metadata.name,
|
||||
),
|
||||
);
|
||||
|
||||
return <DocsCardGrid entities={entities} />;
|
||||
return (
|
||||
<Content>
|
||||
{(groups || [allEntitiesGroup]).map((group, index: number) => (
|
||||
<EntityListDocsGridGroup
|
||||
entities={entities}
|
||||
group={group}
|
||||
key={`${group.title}-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user