feat(techdocs): implement composable home page

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2021-08-05 16:37:23 -04:00
parent 10ef115554
commit 80582cbec4
18 changed files with 317 additions and 62 deletions
+15 -2
View File
@@ -1,5 +1,18 @@
---
'@backstage/plugin-techdocs': minor
'@backstage/plugin-techdocs': patch
---
Rework `TechDocsHome` to use `EntityListProvider` with support for starring docs and filtering on owned, starred, owner, and tags.
Expose a new composable `TechDocsIndexPage` and a `DefaultTechDocsHome` with support for starring docs and filtering on owned, starred, owner, and tags.
You can migrate to the new UI view by making the following changes in your `App.tsx`:
```diff
- <Route path="/docs" element={<TechdocsPage />} />
+ <Route path="/docs" element={<TechDocsIndexPage />}>
+ <DefaultTechDocsHome />
+ </Route>
+ <Route
+ path="/docs/:namespace/:kind/:name/*"
+ element={<TechDocsReaderPage />}
+ />
```
+18
View File
@@ -0,0 +1,18 @@
---
'@backstage/create-app': patch
---
Use new composable `TechDocsIndexPage` and `DefaultTechDocsHome`
Make the following changes to your `App.tsx` to migrate existing apps:
```diff
- <Route path="/docs" element={<TechdocsPage />} />
+ <Route path="/docs" element={<TechDocsIndexPage />}>
+ <DefaultTechDocsHome />
+ </Route>
+ <Route
+ path="/docs/:namespace/:kind/:name/*"
+ element={<TechDocsReaderPage />}
+ />
```
+12 -2
View File
@@ -52,7 +52,11 @@ import {
} from '@backstage/plugin-scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import { TechdocsPage } from '@backstage/plugin-techdocs';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
import AlarmIcon from '@material-ui/icons/Alarm';
import React from 'react';
@@ -116,7 +120,13 @@ const routes = (
{entityPage}
</Route>
<Route path="/catalog-import" element={<CatalogImportPage />} />
<Route path="/docs" element={<TechdocsPage />} />
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<EntityPickerFieldExtension />
@@ -13,7 +13,11 @@ import {
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import { TechdocsPage } from '@backstage/plugin-techdocs';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
import { apis } from './apis';
import { entityPage } from './components/catalog/EntityPage';
@@ -50,7 +54,13 @@ const routes = (
>
{entityPage}
</Route>
<Route path="/docs" element={<TechdocsPage />} />
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
<Route path="/create" element={<ScaffolderPage />} />
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route
+23 -31
View File
@@ -24,9 +24,7 @@ import { UserListFilterKind } from '@backstage/plugin-catalog-react';
// Warning: (ae-missing-release-tag) "createCopyDocsUrlAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
function createCopyDocsUrlAction(
copyToClipboard: Function,
): (
function createCopyDocsUrlAction(copyToClipboard: Function): (
row: DocsTableRow,
) => {
icon: () => JSX.Element;
@@ -50,9 +48,7 @@ function createOwnerColumn(): TableColumn<DocsTableRow>;
function createStarEntityAction(
isStarredEntity: Function,
toggleStarredEntity: Function,
): ({
entity,
}: DocsTableRow) => {
): ({ entity }: DocsTableRow) => {
cellStyle: {
paddingLeft: string;
};
@@ -66,6 +62,19 @@ function createStarEntityAction(
// @public (undocumented)
function createTypeColumn(): TableColumn<DocsTableRow>;
// Warning: (ae-missing-release-tag) "DefaultTechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const DefaultTechDocsHome: ({
initialFilter,
columns,
actions,
}: {
initialFilter?: UserListFilterKind | undefined;
columns?: TableColumn<DocsTableRow>[] | undefined;
actions?: TableProps<DocsTableRow>['actions'];
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "DocsCardGrid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -235,39 +244,22 @@ export const TechDocsCustomHome: ({
tabsConfig: TabsConfig;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-missing-release-tag) "TechDocsIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsHome: ({
initialFilter,
columns,
actions,
}: {
initialFilter?: UserListFilterKind | undefined;
columns?: TableColumn<DocsTableRow>[] | undefined;
actions?:
| (
| Action<DocsTableRow>
| {
action: (rowData: DocsTableRow) => Action<DocsTableRow>;
position: string;
}
| ((rowData: DocsTableRow) => Action<DocsTableRow>)
)[]
| undefined;
}) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "TechDocsHomeLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsHomeLayout: ({ children }: Props) => JSX.Element;
export const TechDocsIndexPage: () => JSX.Element;
// Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechdocsPage: () => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsPageWrapper: ({ children }: Props) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+1 -2
View File
@@ -45,7 +45,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@material-ui/styles": "^4.10.0",
"@types/react": "^16.9",
"@types/react": "*",
"dompurify": "^2.2.9",
"event-source-polyfill": "^1.0.25",
"lodash": "^4.17.21",
@@ -70,7 +70,6 @@
"@types/dompurify": "^2.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react": "*",
"canvas": "^2.6.1",
"cross-fetch": "^3.0.6",
"msw": "^0.29.0"
+7 -4
View File
@@ -23,8 +23,8 @@ import {
rootDocsRouteRef,
rootCatalogDocsRouteRef,
} from './routes';
import { TechDocsHome } from './home/components/TechDocsHome';
import { TechDocsPage } from './reader/components/TechDocsPage';
import { TechDocsIndexPage } from './home/components/TechDocsIndexPage';
import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage';
import { EntityPageDocs } from './EntityPageDocs';
import { MissingAnnotationEmptyState } from '@backstage/core-components';
@@ -33,8 +33,11 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
export const Router = () => {
return (
<Routes>
<Route path={`/${rootRouteRef.path}`} element={<TechDocsHome />} />
<Route path={`/${rootDocsRouteRef.path}`} element={<TechDocsPage />} />
<Route path={`/${rootRouteRef.path}`} element={<TechDocsIndexPage />} />
<Route
path={`/${rootDocsRouteRef.path}`}
element={<TechDocsReaderPage />}
/>
</Routes>
);
};
@@ -18,7 +18,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { TechDocsHome } from './TechDocsHome';
import { DefaultTechDocsHome } from './DefaultTechDocsHome';
import {
ApiProvider,
@@ -71,7 +71,7 @@ describe('TechDocs Home', () => {
it('should render a TechDocs home page', async () => {
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsHome />
<DefaultTechDocsHome />
</ApiProvider>,
);
@@ -35,11 +35,11 @@ import {
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { EntityListDocsTable } from './EntityListDocsTable';
import { TechDocsHomeLayout } from './TechDocsHomeLayout';
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
import { TechDocsPicker } from './TechDocsPicker';
import { DocsTableRow } from './types';
export const TechDocsHome = ({
export const DefaultTechDocsHome = ({
initialFilter = 'all',
columns,
actions,
@@ -49,7 +49,7 @@ export const TechDocsHome = ({
actions?: TableProps<DocsTableRow>['actions'];
}) => {
return (
<TechDocsHomeLayout>
<TechDocsPageWrapper>
<Content>
<ContentHeader title="">
<SupportButton>
@@ -70,6 +70,6 @@ export const TechDocsHome = ({
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</TechDocsHomeLayout>
</TechDocsPageWrapper>
);
};
@@ -0,0 +1,82 @@
/*
* 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 { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { LegacyTechDocsHome } from './LegacyTechDocsHome';
import {
ApiProvider,
ApiRegistry,
ConfigReader,
} from '@backstage/core-app-api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
return {
...actual,
useOwnUser: () => 'test-user',
};
});
const mockCatalogApi = {
getEntityByName: jest.fn(),
getEntities: async () => ({
items: [
{
apiVersion: 'version',
kind: 'User',
metadata: {
name: 'owned',
namespace: 'default',
},
},
],
}),
} as Partial<CatalogApi>;
describe('Legacy TechDocs Home', () => {
const configApi: ConfigApi = new ConfigReader({
organization: {
name: 'My Company',
},
});
const apiRegistry = ApiRegistry.from([
[catalogApiRef, mockCatalogApi],
[configApiRef, configApi],
]);
it('should render a TechDocs home page', async () => {
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<LegacyTechDocsHome />
</ApiProvider>,
);
// Header
expect(await screen.findByText('Documentation')).toBeInTheDocument();
expect(
await screen.findByText(/Documentation available in My Company/i),
).toBeInTheDocument();
// Explore Content
expect(await screen.findByTestId('docs-explore')).toBeDefined();
});
});
@@ -0,0 +1,55 @@
/*
* 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 { PanelType, TechDocsCustomHome } from './TechDocsCustomHome';
export const LegacyTechDocsHome = () => {
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} />;
};
@@ -28,7 +28,7 @@ import {
import { Entity } from '@backstage/catalog-model';
import { DocsTable } from './DocsTable';
import { DocsCardGrid } from './DocsCardGrid';
import { TechDocsHomeLayout } from './TechDocsHomeLayout';
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
import {
CodeSnippet,
@@ -149,17 +149,17 @@ export const TechDocsCustomHome = ({
if (loading) {
return (
<TechDocsHomeLayout>
<TechDocsPageWrapper>
<Content>
<Progress />
</Content>
</TechDocsHomeLayout>
</TechDocsPageWrapper>
);
}
if (error) {
return (
<TechDocsHomeLayout>
<TechDocsPageWrapper>
<Content>
<WarningPanel
severity="error"
@@ -168,12 +168,12 @@ export const TechDocsCustomHome = ({
<CodeSnippet language="text" text={error.toString()} />
</WarningPanel>
</Content>
</TechDocsHomeLayout>
</TechDocsPageWrapper>
);
}
return (
<TechDocsHomeLayout>
<TechDocsPageWrapper>
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
@@ -192,6 +192,6 @@ export const TechDocsCustomHome = ({
/>
))}
</Content>
</TechDocsHomeLayout>
</TechDocsPageWrapper>
);
};
@@ -0,0 +1,44 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { useOutlet } from 'react-router';
import { TechDocsIndexPage } from './TechDocsIndexPage';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useOutlet: jest.fn().mockReturnValue('Route Children'),
}));
jest.mock('./LegacyTechDocsHome', () => ({
LegacyTechDocsHome: jest.fn().mockReturnValue('LegacyTechDocsHomeMock'),
}));
describe('TechDocsIndexPage', () => {
it('renders provided router element', async () => {
const { getByText } = await renderInTestApp(<TechDocsIndexPage />);
expect(getByText('Route Children')).toBeInTheDocument();
});
it('renders legacy TechDocs home when no router children are provided', async () => {
(useOutlet as jest.Mock).mockReturnValueOnce(null);
const { getByText } = await renderInTestApp(<TechDocsIndexPage />);
expect(getByText('LegacyTechDocsHomeMock')).toBeInTheDocument();
});
});
@@ -0,0 +1,25 @@
/*
* 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 { useOutlet } from 'react-router';
import { LegacyTechDocsHome } from './LegacyTechDocsHome';
export const TechDocsIndexPage = () => {
const outlet = useOutlet();
return outlet || <LegacyTechDocsHome />;
};
@@ -23,7 +23,7 @@ type Props = {
children?: React.ReactNode;
};
export const TechDocsHomeLayout = ({ children }: Props) => {
export const TechDocsPageWrapper = ({ children }: Props) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
@@ -15,7 +15,8 @@
*/
export { EntityListDocsTable } from './EntityListDocsTable';
export { TechDocsHomeLayout } from './TechDocsHomeLayout';
export { DefaultTechDocsHome } from './DefaultTechDocsHome';
export { TechDocsPageWrapper } from './TechDocsPageWrapper';
export { TechDocsPicker } from './TechDocsPicker';
export type { PanelType } from './TechDocsCustomHome';
export type { DocsTableRow } from './types';
+3 -2
View File
@@ -21,7 +21,8 @@ export { TechDocsClient, TechDocsStorageClient } from './client';
export type { DocsTableRow, PanelType } from './home/components';
export {
EntityListDocsTable,
TechDocsHomeLayout,
DefaultTechDocsHome,
TechDocsPageWrapper,
TechDocsPicker,
} from './home/components';
export * from './components/DocsResultListItem';
@@ -30,7 +31,7 @@ export {
DocsTable,
EntityTechdocsContent,
TechDocsCustomHome,
TechDocsHome,
TechDocsIndexPage,
TechdocsPage,
techdocsPlugin as plugin,
techdocsPlugin,
+4 -2
View File
@@ -113,10 +113,12 @@ export const TechDocsCustomHome = techdocsPlugin.provide(
}),
);
export const TechDocsHome = techdocsPlugin.provide(
export const TechDocsIndexPage = techdocsPlugin.provide(
createRoutableExtension({
component: () =>
import('./home/components/TechDocsHome').then(m => m.TechDocsHome),
import('./home/components/TechDocsIndexPage').then(
m => m.TechDocsIndexPage,
),
mountPoint: rootRouteRef,
}),
);