Merge pull request #5371 from adrianke77/techdocs-landing-page-customization
Add TechDocs landing page customization and exported components
This commit is contained in:
+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();
|
||||
});
|
||||
+32
-42
@@ -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,21 @@ 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}
|
||||
options={{
|
||||
paging: true,
|
||||
pageSize: 20,
|
||||
search: true,
|
||||
}}
|
||||
data={documents}
|
||||
columns={columns}
|
||||
title={`Owned (${ownedDocuments.length})`}
|
||||
title={
|
||||
title
|
||||
? `${title} (${documents.length})`
|
||||
: `All (${documents.length})`
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
@@ -130,6 +120,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, PanelType } 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',
|
||||
panels: [
|
||||
{
|
||||
title: 'First Tab',
|
||||
description: 'First Tab Description',
|
||||
panelType: 'DocsCardGrid' as PanelType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Second Tab ',
|
||||
panels: [
|
||||
{
|
||||
title: 'Second Tab',
|
||||
description: 'Second Tab Description',
|
||||
panelType: 'DocsTable' as PanelType,
|
||||
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,168 @@
|
||||
/*
|
||||
* 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 { CSSProperties } from '@material-ui/styles';
|
||||
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 panels = {
|
||||
DocsTable: DocsTable,
|
||||
DocsCardGrid: DocsCardGrid,
|
||||
};
|
||||
|
||||
export type PanelType = 'DocsCardGrid' | 'DocsTable';
|
||||
|
||||
export interface PanelConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
panelType: PanelType;
|
||||
panelCSS?: CSSProperties;
|
||||
filterPredicate: (entity: Entity) => boolean;
|
||||
}
|
||||
|
||||
export interface TabConfig {
|
||||
label: string;
|
||||
panels: PanelConfig[];
|
||||
}
|
||||
|
||||
export type TabsConfig = TabConfig[];
|
||||
|
||||
const CustomPanel = ({
|
||||
config,
|
||||
entities,
|
||||
index,
|
||||
}: {
|
||||
config: PanelConfig;
|
||||
entities: Entity[];
|
||||
index: number;
|
||||
}) => {
|
||||
const useStyles = makeStyles({
|
||||
panelContainer: {
|
||||
marginBottom: '2rem',
|
||||
...(config.panelCSS ? config.panelCSS : {}),
|
||||
},
|
||||
});
|
||||
const classes = useStyles();
|
||||
const Panel = panels[config.panelType];
|
||||
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.panelContainer}>
|
||||
<Panel 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.panels.map((config, index) => (
|
||||
<CustomPanel
|
||||
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 { PanelType, 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',
|
||||
panels: [
|
||||
{
|
||||
title: 'Overview',
|
||||
description:
|
||||
'Explore your internal technical ecosystem through documentation.',
|
||||
panelType: 'DocsCardGrid' as PanelType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Owned Documents',
|
||||
panels: [
|
||||
{
|
||||
title: 'Owned documents',
|
||||
description: 'Access your documentation.',
|
||||
panelType: 'DocsTable' as PanelType,
|
||||
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 { PanelType } 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