diff --git a/.changeset/techdocs-quiet-pens-prove.md b/.changeset/techdocs-quiet-pens-prove.md
new file mode 100644
index 0000000000..379c20e72a
--- /dev/null
+++ b/.changeset/techdocs-quiet-pens-prove.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Add customization and exportable components for TechDocs landing page
diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md
index 2cb769b0e3..a76905d6c5 100644
--- a/docs/features/techdocs/how-to-guides.md
+++ b/docs/features/techdocs/how-to-guides.md
@@ -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 = (
+
+ }
+ />
+ }
+ />
+
+```
+
+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`.
diff --git a/plugins/techdocs/src/home/components/OverviewContent.test.tsx b/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx
similarity index 77%
rename from plugins/techdocs/src/home/components/OverviewContent.test.tsx
rename to plugins/techdocs/src/home/components/DocsCardGrid.test.tsx
index ec67c346d7..1ddaa4c373 100644
--- a/plugins/techdocs/src/home/components/OverviewContent.test.tsx
+++ b/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx
@@ -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(
- {
/>,
),
);
-
- 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();
});
diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.tsx b/plugins/techdocs/src/home/components/DocsCardGrid.tsx
new file mode 100644
index 0000000000..47f747442c
--- /dev/null
+++ b/plugins/techdocs/src/home/components/DocsCardGrid.tsx
@@ -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 (
+
+ {!entities?.length
+ ? null
+ : entities.map((entity, index: number) => (
+
+
+
+
+ {entity.metadata.description}
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/plugins/techdocs/src/home/components/OwnedContent.test.tsx b/plugins/techdocs/src/home/components/DocsTable.test.tsx
similarity index 68%
rename from plugins/techdocs/src/home/components/OwnedContent.test.tsx
rename to plugins/techdocs/src/home/components/DocsTable.test.tsx
index edccb0d3c6..8a7d8d9af1 100644
--- a/plugins/techdocs/src/home/components/OwnedContent.test.tsx
+++ b/plugins/techdocs/src/home/components/DocsTable.test.tsx
@@ -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(
- {
),
);
- 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(),
- );
+ const { findByText } = render(wrapInTestApp());
expect(await findByText('No documents to show')).toBeInTheDocument();
});
diff --git a/plugins/techdocs/src/home/components/OwnedContent.tsx b/plugins/techdocs/src/home/components/DocsTable.tsx
similarity index 67%
rename from plugins/techdocs/src/home/components/OwnedContent.tsx
rename to plugins/techdocs/src/home/components/DocsTable.tsx
index f962147345..341ff852be 100644
--- a/plugins/techdocs/src/home/components/OwnedContent.tsx
+++ b/plugins/techdocs/src/home/components/DocsTable.tsx
@@ -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 (
-
-
- Discover documentation you own.
-
- {ownedDocuments && ownedDocuments.length > 0 ? (
+ <>
+ {documents && documents.length > 0 ? (
) : (
)}
-
+ >
);
};
diff --git a/plugins/techdocs/src/home/components/OverviewContent.tsx b/plugins/techdocs/src/home/components/OverviewContent.tsx
deleted file mode 100644
index e0030691f5..0000000000
--- a/plugins/techdocs/src/home/components/OverviewContent.tsx
+++ /dev/null
@@ -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 (
-
-
- Discover documentation in your ecosystem.
-
-
- {!entities?.length
- ? null
- : entities.map((entity, index: number) => (
-
-
-
-
- {entity.metadata.description}
-
-
-
-
- ))}
-
-
- );
-};
diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx
new file mode 100644
index 0000000000..a7f4d90240
--- /dev/null
+++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx
@@ -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 = {
+ 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(
+
+
+ ,
+ );
+
+ // 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();
+ });
+});
diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx
new file mode 100644
index 0000000000..37a9dadddb
--- /dev/null
+++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx
@@ -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 (
+ <>
+
+ {index === 0 ? (
+
+ Discover documentation in your ecosystem.
+
+ ) : null}
+
+
+
+
+ >
+ );
+};
+
+export const TechDocsCustomHome = ({
+ tabsConfig,
+}: {
+ tabsConfig: TabsConfig;
+}) => {
+ const [selectedTab, setSelectedTab] = useState(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 (
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ setSelectedTab(index)}
+ tabs={tabsConfig.map(({ label }, index) => ({
+ id: index.toString(),
+ label,
+ }))}
+ />
+
+ {currentTabConfig.widgets.map((config, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsHome.test.tsx
index 8067767d39..566ed3bd0b 100644
--- a/plugins/techdocs/src/home/components/TechDocsHome.test.tsx
+++ b/plugins/techdocs/src/home/components/TechDocsHome.test.tsx
@@ -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 = {
getEntities: async () => ({ items: [] }),
diff --git a/plugins/techdocs/src/home/components/TechDocsHome.tsx b/plugins/techdocs/src/home/components/TechDocsHome.tsx
index 5e9e25e7a0..904c759b11 100644
--- a/plugins/techdocs/src/home/components/TechDocsHome.tsx
+++ b/plugins/techdocs/src/home/components/TechDocsHome.tsx
@@ -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(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 (
-
-
-
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
-
- setSelectedTab(index)}
- tabs={tabs.map(({ label }, index) => ({
- id: index.toString(),
- label,
- }))}
- />
- {selectedTab === 0 ? (
-
- ) : (
-
- )}
-
- );
+ 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 ;
};
diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts
index 30c143e31f..f72938e385 100644
--- a/plugins/techdocs/src/index.ts
+++ b/plugins/techdocs/src/index.ts
@@ -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';
diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts
index a945ca1494..d9b38a2c76 100644
--- a/plugins/techdocs/src/plugin.ts
+++ b/plugins/techdocs/src/plugin.ts
@@ -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,
+ }),
+);