diff --git a/.changeset/techdocs-angry-poems-doubt.md b/.changeset/techdocs-angry-poems-doubt.md new file mode 100644 index 0000000000..ba763a031c --- /dev/null +++ b/.changeset/techdocs-angry-poems-doubt.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Adds support for being able to customize and compose your TechDocs reader page in the App. + +You can likely upgrade to this version without issue. If, however, you have +imported the `` component in your custom code, the name of a property +has changed. You will need to make the following change anywhere you use it: + +```diff +- ++ +``` diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index db093caa39..9c41c6dde2 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -142,6 +142,84 @@ const AppRoutes = () => { }; ``` +## How to customize the TechDocs reader page? + +Similar to how it is possible to customize the TechDocs Home, it is also +possible to customize the TechDocs Reader Page. It is done in your `app` +package. By default, you might see something like this in your `App.tsx`: + +```tsx +const AppRoutes = () => { + }> + {techDocsPage} + ; +}; +``` + +The `techDocsPage` is a default techdocs reader page which lives in +`packages/app/src/components/techdocs`. It includes the following without you +having to set anything up. + +```tsx + + {({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => ( + <> + + + + + + )} + +``` + +If you would like to compose your own `techDocsPage`, you can do so by replacing +the children of TechDocsPage with something else. Maybe you are _just_ +interested in replacing the Header: + +```tsx + + {({ entityRef, onReady }) => ( + <> +
+ + + + + )} + +``` + +Or maybe you want to disable the in-context search + +```tsx + + {({ entityRef, onReady }) => ( + <> +
+ + + + + )} + +``` + +Or maybe you want to replace the entire TechDocs Page. + +```tsx + +
+ +

my own content

+
+ +``` + ## How to migrate from TechDocs Alpha to Beta > This guide only applies to the "recommended" TechDocs deployment method (where diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 87984bece0..109ae7922e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -84,6 +84,8 @@ import { searchPage } from './components/search/SearchPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; +import { techDocsPage } from './components/techdocs/TechDocsPage'; + const app = createApp({ apis, plugins: Object.values(plugins), @@ -170,7 +172,9 @@ const routes = ( } - /> + > + {techDocsPage} + }> diff --git a/packages/app/src/components/techdocs/TechDocsPage.tsx b/packages/app/src/components/techdocs/TechDocsPage.tsx new file mode 100644 index 0000000000..be11e30e61 --- /dev/null +++ b/packages/app/src/components/techdocs/TechDocsPage.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Content } from '@backstage/core-components'; +import { + TechDocsPageHeader, + TechDocsPage, + Reader, +} from '@backstage/plugin-techdocs'; +import React from 'react'; + +const DefaultTechDocsPage = () => { + return ( + + {({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => ( + <> + + + + + + )} + + ); +}; + +export const techDocsPage = ; diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 087cdf803a..b091d37743 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -182,7 +182,7 @@ export type PanelType = 'DocsCardGrid' | 'DocsTable'; // // @public (undocumented) export const Reader: ({ - entityId, + entityRef, onReady, withSearch, }: Props_3) => JSX.Element; @@ -264,11 +264,55 @@ export const TechDocsCustomHome: ({ // @public (undocumented) export const TechDocsIndexPage: () => JSX.Element; +// Warning: (ae-missing-release-tag) "TechDocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsPage: ({ children }: TechDocsPageProps) => JSX.Element; + // Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const TechdocsPage: () => JSX.Element; +// Warning: (ae-missing-release-tag) "TechDocsPageHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TechDocsPageHeader: ({ + entityRef, + entityMetadata, + techDocsMetadata, +}: TechDocsPageHeaderProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "TechDocsPageHeaderProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TechDocsPageHeaderProps = { + entityRef: EntityName; + entityMetadata?: TechDocsEntityMetadata; + techDocsMetadata?: TechDocsMetadata; +}; + +// Warning: (ae-missing-release-tag) "TechDocsPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TechDocsPageProps = { + children?: TechDocsPageRenderFunction | React_2.ReactNode; +}; + +// Warning: (ae-missing-release-tag) "TechDocsPageRenderFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TechDocsPageRenderFunction = ({ + techdocsMetadataValue, + entityMetadataValue, + entityRef, +}: { + techdocsMetadataValue?: TechDocsMetadata | undefined; + entityMetadataValue?: TechDocsEntityMetadata | undefined; + entityRef: EntityName; + onReady: () => void; +}) => JSX.Element; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -301,7 +345,9 @@ export { techdocsPlugin }; // Warning: (ae-missing-release-tag) "TechDocsReaderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const TechDocsReaderPage: () => JSX.Element; +export const TechDocsReaderPage: ({ + children, +}: TechDocsPageProps) => JSX.Element; // Warning: (ae-missing-release-tag) "TechDocsStorageApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 659a4e2caf..8dc0e5d30b 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -110,7 +110,7 @@ function createPage({ render() { return ( { return ( { + const [documentReady, setDocumentReady] = useState(false); + const { namespace, kind, name } = useParams(); + + const techdocsApi = useApi(techdocsApiRef); + + const { value: techdocsMetadataValue } = useAsync(() => { + if (documentReady) { + return techdocsApi.getTechDocsMetadata({ kind, namespace, name }); + } + + return Promise.resolve(undefined); + }, [kind, namespace, name, techdocsApi, documentReady]); + + const { value: entityMetadataValue, error: entityMetadataError } = + useAsync(() => { + return techdocsApi.getEntityMetadata({ kind, namespace, name }); + }, [kind, namespace, name, techdocsApi]); + + const onReady = useCallback(() => { + setDocumentReady(true); + }, [setDocumentReady]); + + if (entityMetadataError) { + return ; + } + + return ( + + + + + + + ); +}; diff --git a/plugins/techdocs/src/reader/components/Reader.test.tsx b/plugins/techdocs/src/reader/components/Reader.test.tsx index 91e5693c43..8b5dbced3f 100644 --- a/plugins/techdocs/src/reader/components/Reader.test.tsx +++ b/plugins/techdocs/src/reader/components/Reader.test.tsx @@ -41,7 +41,7 @@ const { useParams }: { useParams: jest.Mock } = describe('', () => { it('should render Reader content', async () => { useParams.mockReturnValue({ - entityId: 'Component::backstage', + entityRef: 'Component::backstage', }); const scmIntegrationsApi: ScmIntegrationsApi = @@ -68,7 +68,7 @@ describe('', () => { wrapInTestApp( void; withSearch?: boolean; }; @@ -71,8 +71,8 @@ const useStyles = makeStyles(theme => ({ }, })); -export const Reader = ({ entityId, onReady, withSearch = true }: Props) => { - const { kind, namespace, name } = entityId; +export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => { + const { kind, namespace, name } = entityRef; const theme = useTheme(); const classes = useStyles(); @@ -448,7 +448,7 @@ export const Reader = ({ entityId, onReady, withSearch = true }: Props) => { {withSearch && shadowDomRef?.current?.shadowRoot?.innerHTML && ( - + )}
diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx index 584ce534fa..6a5af982fd 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx @@ -22,6 +22,7 @@ import { scmIntegrationsApiRef, } from '@backstage/integration-react'; import { wrapInTestApp } from '@backstage/test-utils'; +import { Header } from '@backstage/core-components'; import { techdocsApiRef, TechDocsApi, @@ -53,7 +54,7 @@ global.scroll = jest.fn(); describe('', () => { it('should render techdocs page', async () => { useParams.mockReturnValue({ - entityId: 'Component::backstage', + entityRef: 'Component::backstage', }); const scmIntegrationsApi: ScmIntegrationsApi = @@ -107,4 +108,69 @@ describe('', () => { expect(rendered.getByTestId('techdocs-content')).toBeInTheDocument(); }); }); + + it('should render techdocs page with custom header', async () => { + useParams.mockReturnValue({ + entityRef: 'Component::backstage', + }); + + const scmIntegrationsApi: ScmIntegrationsApi = + ScmIntegrationsApi.fromConfig( + new ConfigReader({ + integrations: {}, + }), + ); + const techdocsApi: Partial = { + getEntityMetadata: () => + Promise.resolve({ + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'backstage', + }, + }), + getTechDocsMetadata: () => + Promise.resolve({ + site_name: 'string', + site_description: 'string', + }), + }; + + const techdocsStorageApi: Partial = { + getEntityDocs: (): Promise => Promise.resolve('String'), + getBaseUrl: (): Promise => Promise.resolve('String'), + getApiOrigin: (): Promise => Promise.resolve('String'), + }; + const searchApi = { + query: () => + Promise.resolve({ + results: [], + }), + }; + const apiRegistry = ApiRegistry.from([ + [scmIntegrationsApiRef, scmIntegrationsApi], + [techdocsApiRef, techdocsApi], + [techdocsStorageApiRef, techdocsStorageApi], + [searchApiRef, searchApi], + ]); + + await act(async () => { + const rendered = render( + wrapInTestApp( + + + {({ techdocsMetadataValue }) => ( +
+ )} + + , + ), + ); + expect(rendered.getByText('A custom header')).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.tsx index 568fca7330..a8db3e1d15 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPage.tsx @@ -15,17 +15,35 @@ */ import React, { useCallback, useState } from 'react'; +import { useOutlet } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { techdocsApiRef } from '../../api'; -import { Reader } from './Reader'; import { TechDocsNotFound } from './TechDocsNotFound'; -import { TechDocsPageHeader } from './TechDocsPageHeader'; - -import { Content, Page } from '@backstage/core-components'; +import { LegacyTechDocsPage } from './LegacyTechDocsPage'; +import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; +import { EntityName } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; +import { Page } from '@backstage/core-components'; + +export type TechDocsPageRenderFunction = ({ + techdocsMetadataValue, + entityMetadataValue, + entityRef, +}: { + techdocsMetadataValue?: TechDocsMetadata | undefined; + entityMetadataValue?: TechDocsEntityMetadata | undefined; + entityRef: EntityName; + onReady: () => void; +}) => JSX.Element; + +export type TechDocsPageProps = { + children?: TechDocsPageRenderFunction | React.ReactNode; +}; + +export const TechDocsPage = ({ children }: TechDocsPageProps) => { + const outlet = useOutlet(); -export const TechDocsPage = () => { const [documentReady, setDocumentReady] = useState(false); const { namespace, kind, name } = useParams(); @@ -52,27 +70,18 @@ export const TechDocsPage = () => { return ; } + if (!children) return outlet || ; + return ( - - - - + {children instanceof Function + ? children({ + techdocsMetadataValue, + entityMetadataValue, + entityRef: { kind, namespace, name }, + onReady, + }) + : children} ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx index dede1af115..4fa43bfed5 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx @@ -25,7 +25,7 @@ describe('', () => { await act(async () => { const rendered = await renderInTestApp( ', () => { await act(async () => { const rendered = await renderInTestApp( ', () => { await act(async () => { const rendered = await renderInTestApp( { - const { name } = entityId; + const { name } = entityRef; const { site_name: siteName, site_description: siteDescription } = techDocsMetadata || {}; @@ -59,7 +59,7 @@ export const TechDocsPageHeader = ({ value={ } diff --git a/plugins/techdocs/src/reader/components/index.ts b/plugins/techdocs/src/reader/components/index.ts index 16136bae76..4f6d151e5d 100644 --- a/plugins/techdocs/src/reader/components/index.ts +++ b/plugins/techdocs/src/reader/components/index.ts @@ -15,3 +15,5 @@ */ export * from './Reader'; +export * from './TechDocsPage'; +export * from './TechDocsPageHeader';