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, + }), +);