refactor(techdocs): rework TechDocsHome to use EntityListProvider

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2021-07-13 17:16:51 -04:00
parent c7be91b571
commit a440d3b389
24 changed files with 451 additions and 120 deletions
@@ -18,24 +18,29 @@ import React from 'react';
import { useCopyToClipboard } from 'react-use';
import { generatePath } from 'react-router-dom';
import { IconButton, Tooltip } from '@material-ui/core';
import ShareIcon from '@material-ui/icons/Share';
import { Entity } from '@backstage/catalog-model';
import { rootDocsRouteRef } from '../../routes';
import {
Table,
EmptyState,
Button,
SubvalueCell,
EmptyState,
Link,
SubvalueCell,
Table,
TableProps,
} from '@backstage/core-components';
import * as actionFactories from './actions';
import { DocsTableRow } from './types';
export const DocsTable = ({
entities,
title,
loading,
actions,
}: {
entities: Entity[] | undefined;
title?: string | undefined;
loading?: boolean | undefined;
actions?: TableProps<DocsTableRow>['actions'];
}) => {
const [, copyToClipboard] = useCopyToClipboard();
@@ -43,15 +48,14 @@ export const DocsTable = ({
const documents = entities.map(entity => {
return {
name: entity.metadata.name,
description: entity.metadata.description,
owner: entity?.spec?.owner,
type: entity?.spec?.type,
docsUrl: generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
}),
entity,
resolved: {
docsUrl: generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
}),
},
};
});
@@ -60,49 +64,43 @@ export const DocsTable = ({
title: 'Document',
field: 'name',
highlight: true,
render: (row: any): React.ReactNode => (
render: (row: DocsTableRow): React.ReactNode => (
<SubvalueCell
value={<Link to={row.docsUrl}>{row.name}</Link>}
subvalue={row.description}
value={
<Link to={row.resolved.docsUrl}>{row.entity.metadata.name}</Link>
}
subvalue={row.entity.metadata.description}
/>
),
},
{
title: 'Owner',
field: 'owner',
field: 'entity.spec.owner',
},
{
title: 'Type',
field: 'type',
},
{
title: 'Actions',
width: '10%',
render: (row: any) => (
<Tooltip title="Click to copy documentation link to clipboard">
<IconButton
onClick={() =>
copyToClipboard(`${window.location.href}/${row.docsUrl}`)
}
>
<ShareIcon />
</IconButton>
</Tooltip>
),
field: 'entity.spec.type',
},
];
const defaultActions: TableProps<DocsTableRow>['actions'] = [
actionFactories.createCopyDocsUrlAction(copyToClipboard),
];
return (
<>
{documents && documents.length > 0 ? (
<Table
{loading || (documents && documents.length > 0) ? (
<Table<DocsTableRow>
isLoading={loading}
options={{
paging: true,
pageSize: 20,
search: true,
actionsColumnIndex: -1,
}}
data={documents}
columns={columns}
actions={actions || defaultActions}
title={
title
? `${title} (${documents.length})`
@@ -0,0 +1,70 @@
/*
* 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 React from 'react';
import { useCopyToClipboard } from 'react-use';
import { capitalize } from 'lodash';
import { CodeSnippet, WarningPanel } from '@backstage/core-components';
import {
favoriteEntityIcon,
favoriteEntityTooltip,
useEntityListProvider,
useStarredEntities,
} from '@backstage/plugin-catalog-react';
import * as actionFactories from './actions';
import { DocsTable } from './DocsTable';
import { DocsTableRow } from './types';
export const EntityListDocsTable = () => {
const { loading, error, entities, filters } = useEntityListProvider();
const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
const [, copyToClipboard] = useCopyToClipboard();
const title = capitalize(filters.user?.value ?? 'all');
const actions = [
actionFactories.createCopyDocsUrlAction(copyToClipboard),
({ entity }: DocsTableRow) => {
const isStarred = isStarredEntity(entity);
return {
cellStyle: { paddingLeft: '1em' },
icon: () => favoriteEntityIcon(isStarred),
tooltip: favoriteEntityTooltip(isStarred),
onClick: () => toggleStarredEntity(entity),
};
},
];
if (error) {
return (
<WarningPanel
severity="error"
title="Could not load available documentation."
>
<CodeSnippet language="text" text={error.toString()} />
</WarningPanel>
);
}
return (
<DocsTable
title={title}
entities={entities}
loading={loading}
actions={actions}
/>
);
};
@@ -28,20 +28,19 @@ import {
import { Entity } from '@backstage/catalog-model';
import { DocsTable } from './DocsTable';
import { DocsCardGrid } from './DocsCardGrid';
import { TechDocsHomeLayout } from './TechDocsHomeLayout';
import {
CodeSnippet,
Content,
Header,
HeaderTabs,
Page,
Progress,
WarningPanel,
SupportButton,
ContentHeader,
} from '@backstage/core-components';
import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
const panels = {
DocsTable: DocsTable,
@@ -122,7 +121,6 @@ export const TechDocsCustomHome = ({
}) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const catalogApi: CatalogApi = useApi(catalogApiRef);
const configApi: ConfigApi = useApi(configApiRef);
const {
value: entities,
@@ -147,27 +145,21 @@ export const TechDocsCustomHome = ({
});
});
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
const currentTabConfig = tabsConfig[selectedTab];
if (loading) {
return (
<Page themeId="documentation">
<Header title="Documentation" subtitle={generatedSubtitle} />
<TechDocsHomeLayout>
<Content>
<Progress />
</Content>
</Page>
</TechDocsHomeLayout>
);
}
if (error) {
return (
<Page themeId="documentation">
<Header title="Documentation" subtitle={generatedSubtitle} />
<TechDocsHomeLayout>
<Content>
<WarningPanel
severity="error"
@@ -176,13 +168,12 @@ export const TechDocsCustomHome = ({
<CodeSnippet language="text" text={error.toString()} />
</WarningPanel>
</Content>
</Page>
</TechDocsHomeLayout>
);
}
return (
<Page themeId="documentation">
<Header title="Documentation" subtitle={generatedSubtitle} />
<TechDocsHomeLayout>
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
@@ -201,6 +192,6 @@ export const TechDocsCustomHome = ({
/>
))}
</Content>
</Page>
</TechDocsHomeLayout>
);
};
@@ -15,7 +15,7 @@
*/
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { TechDocsHome } from './TechDocsHome';
@@ -25,7 +25,11 @@ import {
ApiRegistry,
ConfigReader,
} from '@backstage/core-app-api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import {
ConfigApi,
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -36,7 +40,7 @@ jest.mock('@backstage/plugin-catalog-react', () => {
});
const mockCatalogApi = {
getEntityByName: jest.fn(),
getEntityByName: () => Promise.resolve(),
getEntities: async () => ({
items: [
{
@@ -61,6 +65,7 @@ describe('TechDocs Home', () => {
const apiRegistry = ApiRegistry.from([
[catalogApiRef, mockCatalogApi],
[configApiRef, configApi],
[storageApiRef, MockStorageApi.create()],
]);
it('should render a TechDocs home page', async () => {
@@ -75,8 +80,5 @@ describe('TechDocs Home', () => {
expect(
await screen.findByText(/Documentation available in My Company/i),
).toBeInTheDocument();
// Explore Content
expect(await screen.findByTestId('docs-explore')).toBeDefined();
});
});
@@ -15,41 +15,49 @@
*/
import React from 'react';
import { PanelType, TechDocsCustomHome } from './TechDocsCustomHome';
import {
Content,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import {
EntityListContainer,
FilterContainer,
FilteredEntityLayout,
} from '@backstage/plugin-catalog';
import {
EntityListProvider,
EntityOwnerPicker,
EntityTagPicker,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { EntityListDocsTable } from './EntityListDocsTable';
import { TechDocsHomeLayout } from './TechDocsHomeLayout';
import { TechDocsPicker } from './TechDocsPicker';
export const TechDocsHome = () => {
const tabsConfig = [
{
label: 'Overview',
panels: [
{
title: 'Overview',
description:
'Explore your internal technical ecosystem through documentation.',
panelType: 'DocsCardGrid' as PanelType,
filterPredicate: () => true,
},
// uncomment this if you would like to have a secondary panel with owned documents
// {
// title: 'Owned',
// description: 'Explore your owned internal documentation.',
// panelType: 'DocsCardGrid' as PanelType,
// filterPredicate: 'ownedByUser',
// },
],
},
{
label: 'Owned Documents',
panels: [
{
title: 'Owned documents',
description: 'Access your documentation.',
panelType: 'DocsTable' as PanelType,
// ownedByUser filters out entities owned by signed in user
filterPredicate: 'ownedByUser',
},
],
},
];
return <TechDocsCustomHome tabsConfig={tabsConfig} />;
return (
<TechDocsHomeLayout>
<Content>
<ContentHeader title="">
<SupportButton>
Discover documentation in your ecosystem.
</SupportButton>
</ContentHeader>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<TechDocsPicker />
<UserListPicker initialFilter="all" />
<EntityOwnerPicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<EntityListDocsTable />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</TechDocsHomeLayout>
);
};
@@ -0,0 +1,41 @@
/*
* 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 React from 'react';
import { PageWithHeader } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
type Props = {
children?: React.ReactNode;
};
export const TechDocsHomeLayout = ({ children }: Props) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
return (
<PageWithHeader
title="Documentation"
subtitle={generatedSubtitle}
themeId="documentation"
>
{children}
</PageWithHeader>
);
};
@@ -0,0 +1,47 @@
/*
* 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 { useEffect } from 'react';
import {
CATALOG_FILTER_EXISTS,
DefaultEntityFilters,
EntityFilter,
useEntityListProvider,
} from '@backstage/plugin-catalog-react';
class TechDocsFilter implements EntityFilter {
getCatalogFilters(): Record<string, string | symbol | (string | symbol)[]> {
return {
'metadata.annotations.backstage.io/techdocs-ref': CATALOG_FILTER_EXISTS,
};
}
}
type CustomFilters = DefaultEntityFilters & {
techdocs?: TechDocsFilter;
};
export const TechDocsPicker = () => {
const { updateFilters } = useEntityListProvider<CustomFilters>();
useEffect(() => {
updateFilters({
techdocs: new TechDocsFilter(),
});
}, [updateFilters]);
return null;
};
@@ -0,0 +1,32 @@
/*
* 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 React from 'react';
import ShareIcon from '@material-ui/icons/Share';
import { DocsTableRow } from './types';
export function createCopyDocsUrlAction(copyToClipboard: Function) {
return (row: DocsTableRow) => {
return {
icon: () => <ShareIcon fontSize="small" />,
tooltip: 'Click to copy documentation link to clipboard',
onClick: () =>
copyToClipboard(
`${window.location.href.split('/?')[0]}/${row.resolved.docsUrl}`,
),
};
};
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
export { EntityListDocsTable } from './EntityListDocsTable';
export { TechDocsHomeLayout } from './TechDocsHomeLayout';
export { TechDocsPicker } from './TechDocsPicker';
export type { PanelType } from './TechDocsCustomHome';
@@ -0,0 +1,24 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
export type DocsTableRow = {
entity: Entity;
resolved: {
docsUrl: string;
};
};
+6 -1
View File
@@ -18,7 +18,12 @@ export * from './api';
export { techdocsApiRef, techdocsStorageApiRef } from './api';
export type { TechDocsApi, TechDocsStorageApi } from './api';
export { TechDocsClient, TechDocsStorageClient } from './client';
export type { PanelType } from './home/components/TechDocsCustomHome';
export type { PanelType } from './home/components';
export {
EntityListDocsTable,
TechDocsHomeLayout,
TechDocsPicker,
} from './home/components';
export * from './components/DocsResultListItem';
export {
DocsCardGrid,