Add TechDocs landing page customization and exported components
Signed-off-by: Chongyang Adrian, Ke <ftt.adrian.ke@grabtaxi.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Add customization and exportable components for TechDocs landing page
|
||||
@@ -82,3 +82,80 @@ Caveat: Currently TechDocs sites built using URL Reader will be cached for 30
|
||||
minutes which means they will not be re-built if new changes are made within 30
|
||||
minutes. This cache invalidation will be replaced by commit timestamp based
|
||||
implementation very soon.
|
||||
|
||||
## How to use a custom TechDocs home page?
|
||||
|
||||
### 1st way: TechDocsCustomHome with a custom configuration
|
||||
|
||||
In your main App.tsx:
|
||||
|
||||
```
|
||||
import {
|
||||
TechDocsCustomHome,
|
||||
WidgetType,
|
||||
TechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const tabsConfig = [
|
||||
{
|
||||
label: 'Custom Tab',
|
||||
widgets: [
|
||||
{
|
||||
title: 'Custom Documents Cards 1',
|
||||
description:
|
||||
'Explore your internal technical ecosystem through documentation.',
|
||||
// sets maximum height of widget, as CSS maxHeight attribute
|
||||
widgetMaxHeight: '400px'
|
||||
widgetType: 'DocsCardGrid' as WidgetType,
|
||||
filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationOne'];
|
||||
},
|
||||
{
|
||||
title: 'Custom Documents Cards 2',
|
||||
description:
|
||||
'Explore your internal technical ecosystem through documentation.',
|
||||
widgetMaxHeight: '400px'
|
||||
widgetType: 'DocsCardGrid' as WidgetType,
|
||||
filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationTwo'];
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Overview',
|
||||
widgets: [
|
||||
{
|
||||
title: 'Overview',
|
||||
description:
|
||||
'Explore your internal technical ecosystem through documentation.',
|
||||
widgetType: 'DocsTable' as WidgetType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route
|
||||
path="/docs"
|
||||
element={<TechDocsCustomHome tabsConfig={tabsConfig} />}
|
||||
/>
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
/>
|
||||
</FlatRoutes>
|
||||
```
|
||||
|
||||
An example of tabsConfig that corresponds to the default documentation home page
|
||||
can be found at `plugins/techdocs/src/home/components/TechDocsHome.tsx`.
|
||||
|
||||
### 2nd way: custom home page plugin
|
||||
|
||||
A custom home page plugin can be built that uses the components extensions
|
||||
DocsCardGrid and DocsTable, exported from @backstage/techdocs. They both take a
|
||||
array of documentation entities ( have 'backstage.io/techdocs-ref' annotation )
|
||||
as an 'entities' attribute.
|
||||
|
||||
For an reference to the React structure of the default home page, please refer
|
||||
to `plugins/techdocs/src/home/components/TechDocsCustomHome.tsx`.
|
||||
|
||||
+5
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,13 +17,13 @@
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { OverviewContent } from './OverviewContent';
|
||||
import { DocsCardGrid } from './DocsCardGrid';
|
||||
|
||||
describe('TechDocs Overview Content', () => {
|
||||
it('should render all TechDocs Documents', async () => {
|
||||
describe('Entity Docs Card Grid', () => {
|
||||
it('should render all entities passed ot it', async () => {
|
||||
const { findByText } = render(
|
||||
wrapInTestApp(
|
||||
<OverviewContent
|
||||
<DocsCardGrid
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
@@ -49,13 +49,6 @@ describe('TechDocs Overview Content', () => {
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(await findByText('Overview')).toBeInTheDocument();
|
||||
expect(
|
||||
await findByText(
|
||||
/Explore your internal technical ecosystem through documentation./i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(await findByText('testName')).toBeInTheDocument();
|
||||
expect(await findByText('testName2')).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2021 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 { generatePath } from 'react-router-dom';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Button, ItemCardGrid, ItemCardHeader } from '@backstage/core';
|
||||
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
|
||||
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
|
||||
export const DocsCardGrid = ({
|
||||
entities,
|
||||
}: {
|
||||
entities: Entity[] | undefined;
|
||||
}) => {
|
||||
if (!entities) return null;
|
||||
return (
|
||||
<ItemCardGrid data-testid="docs-explore">
|
||||
{!entities?.length
|
||||
? null
|
||||
: entities.map((entity, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardMedia>
|
||||
<ItemCardHeader title={entity.metadata.name} />
|
||||
</CardMedia>
|
||||
<CardContent>{entity.metadata.description}</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
to={generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
color="primary"
|
||||
>
|
||||
Read Docs
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
);
|
||||
};
|
||||
+8
-36
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,37 +16,13 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { OwnedContent } from './OwnedContent';
|
||||
import { DocsTable } from './DocsTable';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useOwnUser: () => {
|
||||
return {
|
||||
value: {
|
||||
apiVersion: 'version',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'owned',
|
||||
namespace: 'default',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'TestKind',
|
||||
name: 'testName',
|
||||
},
|
||||
type: 'ownerOf',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('TechDocs Owned Content', () => {
|
||||
it('should render TechDocs Owned Documents', async () => {
|
||||
const { findByText, queryByText } = render(
|
||||
describe('DocsTable test', () => {
|
||||
it('should render documents passed', async () => {
|
||||
const { findByText } = render(
|
||||
wrapInTestApp(
|
||||
<OwnedContent
|
||||
<DocsTable
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
@@ -93,16 +69,12 @@ describe('TechDocs Owned Content', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(await findByText('Owned documents')).toBeInTheDocument();
|
||||
expect(await findByText(/Access your documentation./i)).toBeInTheDocument();
|
||||
expect(await findByText('testName')).toBeInTheDocument();
|
||||
expect(await queryByText('testName2')).not.toBeInTheDocument();
|
||||
expect(await findByText('testName2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state if no owned documents exist', async () => {
|
||||
const { findByText } = render(
|
||||
wrapInTestApp(<OwnedContent entities={[]} />),
|
||||
);
|
||||
const { findByText } = render(wrapInTestApp(<DocsTable entities={[]} />));
|
||||
|
||||
expect(await findByText('No documents to show')).toBeInTheDocument();
|
||||
});
|
||||
+27
-41
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,46 +20,34 @@ import { generatePath } from 'react-router-dom';
|
||||
|
||||
import { IconButton, Tooltip } from '@material-ui/core';
|
||||
import ShareIcon from '@material-ui/icons/Share';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
Table,
|
||||
EmptyState,
|
||||
Button,
|
||||
SubvalueCell,
|
||||
Link,
|
||||
} from '@backstage/core';
|
||||
import { Table, EmptyState, Button, SubvalueCell, Link } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { isOwnerOf } from '@backstage/plugin-catalog-react';
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
import { useOwnUser } from '../hooks';
|
||||
|
||||
export const OwnedContent = ({
|
||||
export const DocsTable = ({
|
||||
entities,
|
||||
title,
|
||||
}: {
|
||||
entities: Entity[] | undefined;
|
||||
title?: string | undefined;
|
||||
}) => {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { value: user } = useOwnUser();
|
||||
|
||||
if (!entities || !user) return null;
|
||||
if (!entities) return null;
|
||||
|
||||
const ownedDocuments = entities
|
||||
.filter((entity: Entity) => isOwnerOf(user, entity))
|
||||
.map(entity => {
|
||||
return {
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -99,19 +87,17 @@ export const OwnedContent = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader
|
||||
title="Owned documents"
|
||||
description="Access your documentation."
|
||||
>
|
||||
<SupportButton>Discover documentation you own.</SupportButton>
|
||||
</ContentHeader>
|
||||
{ownedDocuments && ownedDocuments.length > 0 ? (
|
||||
<>
|
||||
{documents && documents.length > 0 ? (
|
||||
<Table
|
||||
options={{ paging: true, pageSize: 20, search: true }}
|
||||
data={ownedDocuments}
|
||||
data={documents}
|
||||
columns={columns}
|
||||
title={`Owned (${ownedDocuments.length})`}
|
||||
title={
|
||||
title
|
||||
? `${title} (${documents.length})`
|
||||
: `All (${documents.length})`
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
@@ -130,6 +116,6 @@ export const OwnedContent = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,73 +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 { generatePath } from 'react-router-dom';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
ItemCardGrid,
|
||||
ItemCardHeader,
|
||||
} from '@backstage/core';
|
||||
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
|
||||
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
|
||||
export const OverviewContent = ({
|
||||
entities,
|
||||
}: {
|
||||
entities: Entity[] | undefined;
|
||||
}) => {
|
||||
if (!entities) return null;
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader
|
||||
title="Overview"
|
||||
description="Explore your internal technical ecosystem through documentation."
|
||||
>
|
||||
<SupportButton>Discover documentation in your ecosystem.</SupportButton>
|
||||
</ContentHeader>
|
||||
<ItemCardGrid data-testid="docs-explore">
|
||||
{!entities?.length
|
||||
? null
|
||||
: entities.map((entity, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardMedia>
|
||||
<ItemCardHeader title={entity.metadata.name} />
|
||||
</CardMedia>
|
||||
<CardContent>{entity.metadata.description}</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
to={generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
color="primary"
|
||||
>
|
||||
Read Docs
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 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 { ApiProvider, ApiRegistry } from '@backstage/core';
|
||||
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 { TechDocsCustomHome, WidgetType } from './TechDocsCustomHome';
|
||||
|
||||
describe('TechDocsCustomHome', () => {
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: async () => ({ items: [] }),
|
||||
};
|
||||
|
||||
const apiRegistry = ApiRegistry.with(catalogApiRef, catalogApi);
|
||||
|
||||
it('should render a TechDocs home page', async () => {
|
||||
const tabsConfig = [
|
||||
{
|
||||
label: 'First Tab',
|
||||
widgets: [
|
||||
{
|
||||
title: 'First Tab',
|
||||
description: 'First Tab Description',
|
||||
widgetType: 'DocsCardGrid' as WidgetType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Second Tab ',
|
||||
widgets: [
|
||||
{
|
||||
title: 'Second Tab',
|
||||
description: 'Second Tab Description',
|
||||
widgetType: 'DocsTable' as WidgetType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<TechDocsCustomHome tabsConfig={tabsConfig} />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
// Header
|
||||
expect(await screen.findByText('Documentation')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/Documentation available in Backstage/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Explore Content
|
||||
expect(await screen.findByTestId('docs-explore')).toBeDefined();
|
||||
|
||||
// Tabs
|
||||
expect(await screen.findAllByText('First Tab')).toHaveLength(2);
|
||||
expect(await screen.findByText('Second Tab')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('First Tab Description'),
|
||||
).toBeInTheDocument();
|
||||
(await screen.findByText('Second Tab')).click();
|
||||
expect(
|
||||
await screen.findByText('Second Tab Description'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2021 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, { useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
|
||||
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
Content,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
Header,
|
||||
HeaderTabs,
|
||||
Page,
|
||||
Progress,
|
||||
useApi,
|
||||
WarningPanel,
|
||||
SupportButton,
|
||||
ContentHeader,
|
||||
} from '@backstage/core';
|
||||
import { DocsTable } from './DocsTable';
|
||||
import { DocsCardGrid } from './DocsCardGrid';
|
||||
|
||||
const widgets = {
|
||||
DocsTable: DocsTable,
|
||||
DocsCardGrid: DocsCardGrid,
|
||||
};
|
||||
|
||||
export type WidgetType = 'DocsCardGrid' | 'DocsTable';
|
||||
|
||||
export interface WidgetConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
widgetType: WidgetType;
|
||||
widgetMaxHeight?: string;
|
||||
filterPredicate: (entity: Entity) => boolean;
|
||||
}
|
||||
|
||||
export interface TabConfig {
|
||||
label: string;
|
||||
widgets: WidgetConfig[];
|
||||
}
|
||||
|
||||
export type TabsConfig = TabConfig[];
|
||||
|
||||
const CustomWidget = ({
|
||||
config,
|
||||
entities,
|
||||
index,
|
||||
}: {
|
||||
config: WidgetConfig;
|
||||
entities: Entity[];
|
||||
index: number;
|
||||
}) => {
|
||||
const useStyles = makeStyles({
|
||||
widgetContainer: {
|
||||
maxHeight: config.widgetMaxHeight || 'inherit',
|
||||
marginBottom: '2rem',
|
||||
overflow: 'auto',
|
||||
},
|
||||
});
|
||||
const classes = useStyles();
|
||||
const Widget = widgets[config.widgetType];
|
||||
const shownEntities = entities.filter(config.filterPredicate);
|
||||
return (
|
||||
<>
|
||||
<ContentHeader title={config.title} description={config.description}>
|
||||
{index === 0 ? (
|
||||
<SupportButton>
|
||||
Discover documentation in your ecosystem.
|
||||
</SupportButton>
|
||||
) : null}
|
||||
</ContentHeader>
|
||||
<div className={classes.widgetContainer}>
|
||||
<Widget entities={shownEntities} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TechDocsCustomHome = ({
|
||||
tabsConfig,
|
||||
}: {
|
||||
tabsConfig: TabsConfig;
|
||||
}) => {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const catalogApi: CatalogApi = useApi(catalogApiRef);
|
||||
const configApi: ConfigApi = useApi(configApiRef);
|
||||
|
||||
const { value: entities, loading, error } = useAsync(async () => {
|
||||
const response = await catalogApi.getEntities();
|
||||
return response.items.filter((entity: Entity) => {
|
||||
return !!entity.metadata.annotations?.['backstage.io/techdocs-ref'];
|
||||
});
|
||||
});
|
||||
|
||||
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} />
|
||||
<Content>
|
||||
<Progress />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
<Header title="Documentation" subtitle={generatedSubtitle} />
|
||||
<Content>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="Could not load available documentation."
|
||||
>
|
||||
<CodeSnippet language="text" text={error.toString()} />
|
||||
</WarningPanel>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
<Header title="Documentation" subtitle={generatedSubtitle} />
|
||||
<HeaderTabs
|
||||
selectedIndex={selectedTab}
|
||||
onChange={index => setSelectedTab(index)}
|
||||
tabs={tabsConfig.map(({ label }, index) => ({
|
||||
id: index.toString(),
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<Content>
|
||||
{currentTabConfig.widgets.map((config, index) => (
|
||||
<CustomWidget
|
||||
key={index}
|
||||
config={config}
|
||||
entities={!!entities ? entities : []}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,30 @@ import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TechDocsHome } from './TechDocsHome';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useOwnUser: () => {
|
||||
return {
|
||||
value: {
|
||||
apiVersion: 'version',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'owned',
|
||||
namespace: 'default',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'TestKind',
|
||||
name: 'testName',
|
||||
},
|
||||
type: 'ownerOf',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('TechDocs Home', () => {
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: async () => ({ items: [] }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,88 +14,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
Content,
|
||||
Header,
|
||||
HeaderTabs,
|
||||
Page,
|
||||
Progress,
|
||||
useApi,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { OverviewContent } from './OverviewContent';
|
||||
import { OwnedContent } from './OwnedContent';
|
||||
import { useOwnUser } from '../hooks';
|
||||
import { isOwnerOf } from '@backstage/plugin-catalog-react';
|
||||
import { WidgetType, TechDocsCustomHome } from './TechDocsCustomHome';
|
||||
|
||||
export const TechDocsHome = () => {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const catalogApi: CatalogApi = useApi(catalogApiRef);
|
||||
const configApi: ConfigApi = useApi(configApiRef);
|
||||
const { value: user } = useOwnUser();
|
||||
|
||||
const tabs = [{ label: 'Overview' }, { label: 'Owned Documents' }];
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
const response = await catalogApi.getEntities();
|
||||
return response.items.filter((entity: Entity) => {
|
||||
return !!entity.metadata.annotations?.['backstage.io/techdocs-ref'];
|
||||
});
|
||||
});
|
||||
|
||||
const generatedSubtitle = `Documentation available in ${
|
||||
configApi.getOptionalString('organization.name') ?? 'Backstage'
|
||||
}`;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
<Header title="Documentation" subtitle={generatedSubtitle} />
|
||||
<Content>
|
||||
<Progress />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
<Header title="Documentation" subtitle={generatedSubtitle} />
|
||||
<Content>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="Could not load available documentation."
|
||||
>
|
||||
<CodeSnippet language="text" text={error.toString()} />
|
||||
</WarningPanel>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
<Header title="Documentation" subtitle={generatedSubtitle} />
|
||||
<HeaderTabs
|
||||
selectedIndex={selectedTab}
|
||||
onChange={index => setSelectedTab(index)}
|
||||
tabs={tabs.map(({ label }, index) => ({
|
||||
id: index.toString(),
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
{selectedTab === 0 ? (
|
||||
<OverviewContent entities={value} />
|
||||
) : (
|
||||
<OwnedContent entities={value} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
const tabsConfig = [
|
||||
{
|
||||
label: 'Overview',
|
||||
widgets: [
|
||||
{
|
||||
title: 'Overview',
|
||||
description:
|
||||
'Explore your internal technical ecosystem through documentation.',
|
||||
widgetType: 'DocsCardGrid' as WidgetType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Owned',
|
||||
widgets: [
|
||||
{
|
||||
title: 'Owned documents',
|
||||
description: 'Access your documentation.',
|
||||
widgetType: 'DocsTable' as WidgetType,
|
||||
filterPredicate: (entity: Entity) => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return isOwnerOf(user, entity);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return <TechDocsCustomHome tabsConfig={tabsConfig} />;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,12 @@ export {
|
||||
techdocsPlugin as plugin,
|
||||
TechdocsPage,
|
||||
EntityTechdocsContent,
|
||||
DocsCardGrid,
|
||||
DocsTable,
|
||||
TechDocsCustomHome,
|
||||
TechDocsReaderPage,
|
||||
} from './plugin';
|
||||
export { Router, EmbeddedDocsRouter } from './Router';
|
||||
export * from './reader';
|
||||
export * from './api';
|
||||
export type { WidgetType } from './home/components/TechDocsCustomHome';
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
createRoutableExtension,
|
||||
createComponentExtension,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
techdocsStorageApiRef,
|
||||
@@ -111,3 +112,41 @@ export const EntityTechdocsContent = techdocsPlugin.provide(
|
||||
mountPoint: rootCatalogDocsRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
// takes a list of entities and renders documentation cards
|
||||
export const DocsCardGrid = techdocsPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./home/components/DocsCardGrid').then(m => m.DocsCardGrid),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// takes a list of entities and renders table listing documentation
|
||||
export const DocsTable = techdocsPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () => import('./home/components/DocsTable').then(m => m.DocsTable),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// takes a custom tabs config object and renders a documentation landing page
|
||||
export const TechDocsCustomHome = techdocsPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () =>
|
||||
import('./home/components/TechDocsCustomHome').then(
|
||||
m => m.TechDocsCustomHome,
|
||||
),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
export const TechDocsReaderPage = techdocsPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () =>
|
||||
import('./reader/components/TechDocsPage').then(m => m.TechDocsPage),
|
||||
mountPoint: rootDocsRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user