diff --git a/.changeset/techdocs-until-you-puke.md b/.changeset/techdocs-until-you-puke.md new file mode 100644 index 0000000000..af70fe315b --- /dev/null +++ b/.changeset/techdocs-until-you-puke.md @@ -0,0 +1,58 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +TechDocs now supports a new method of customization: addons! + +To customize the standalone TechDocs reader page experience, update your `/packages/app/src/App.tsx` in the following way: + +```diff +import { TechDocsIndexPage, TechDocsReaderPage } from '@backstage/plugin-techdocs'; ++ import { TechDocsAddons } from '@backstage/plugin-techdocs-addons'; ++ import { SomeAddon } from '@backstage/plugin-some-plugin'; +- import { techDocsPage } from './components/techdocs/TechDocsPage'; + +// ... + + } /> + } + > +- {techDocsPage} ++ ++ ++ + + +// ... +``` + +To customize the TechDocs reader experience on the Catalog entity page, update your `packages/app/src/components/catalog/EntityPage.tsx` in the following way: + +```diff +import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; ++ import { TechDocsAddons } from '@backstage/plugin-techdocs-addons'; ++ import { SomeAddon } from '@backstage/plugin-some-plugin'; + +// ... + + + + {overviewContent} + + + +- ++ ++ ++ ++ ++ + + + +// ... +``` + +If you do not wish to customize your TechDocs reader experience in this way at this time, no changes are necessary! diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 26068c6c8f..534b7eacda 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -138,6 +138,14 @@ import { import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd'; import React, { ReactNode, useMemo, useState } from 'react'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-addons'; +import { + ExampleContent, + ExampleHeader, + ExamplePrimarySidebar, + ExampleSecondarySidebar, + ExampleSubHeader, +} from '../techdocs/ExampleAddons'; const customEntityFilterKind = ['Component', 'API', 'System']; @@ -397,7 +405,15 @@ const serviceEntityPage = ( - + + + + + + + + + - + + + + + + + + + - + + + + + + + + + diff --git a/plugins/techdocs-addons/api-report.md b/plugins/techdocs-addons/api-report.md index 5f7f2e8cd0..77c9f4d276 100644 --- a/plugins/techdocs-addons/api-report.md +++ b/plugins/techdocs-addons/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { AsyncState } from 'react-use/lib/useAsyncFn'; import { ComponentType } from 'react'; import { Entity } from '@backstage/catalog-model'; @@ -63,6 +61,8 @@ export const TechDocsReaderPage: ( // @public (undocumented) export type TechDocsReaderPageProps = { + hideHeader?: boolean; + addonConfig?: React_2.ReactNode; dom: Element | null; asyncEntityMetadata: AsyncState; asyncTechDocsMetadata: AsyncState; diff --git a/plugins/techdocs-addons/src/addons.tsx b/plugins/techdocs-addons/src/addons.tsx index afb3b151c0..2151abd948 100644 --- a/plugins/techdocs-addons/src/addons.tsx +++ b/plugins/techdocs-addons/src/addons.tsx @@ -21,7 +21,13 @@ import { Extension, useElementFilter, } from '@backstage/core-plugin-api'; -import React, { ComponentType, useCallback } from 'react'; +import React, { + ComponentType, + createContext, + PropsWithChildren, + useCallback, + useContext, +} from 'react'; import { useOutlet } from 'react-router-dom'; import { TechDocsAddonLocations, TechDocsAddonOptions } from './types'; @@ -90,8 +96,30 @@ const getAllTechDocsAddonsData = (collection: ElementCollection) => { }); }; +type TechDocsAddonConfig = { + config?: React.ReactNode | null; +}; + +const TechDocsAddonConfigContext = createContext({}); + +export const TechDocsAddonConfigProvider = ( + props: PropsWithChildren<{ config?: React.ReactNode }>, +) => { + const fromOutlet = useOutlet(); + const config = props.config ?? fromOutlet; + return ( + + {props.children} + + ); +}; + +const useTechDocsAddonsConfig = (): React.ReactNode | null => { + return useContext(TechDocsAddonConfigContext).config || null; +}; + export const useTechDocsAddons = () => { - const node = useOutlet(); + const node = useTechDocsAddonsConfig(); const collection = useElementFilter(node, getAllTechDocsAddons); const options = useElementFilter(node, getAllTechDocsAddonsData); diff --git a/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx index d9d54e2248..2941278feb 100644 --- a/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -18,6 +18,7 @@ import { Page } from '@backstage/core-components'; import React from 'react'; import { useParams } from 'react-router-dom'; import { AsyncState } from 'react-use/lib/useAsyncFn'; +import { TechDocsAddonConfigProvider } from '../../addons'; import { TechDocsMetadataProvider, @@ -33,6 +34,8 @@ import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; * @public */ export type TechDocsReaderPageProps = { + hideHeader?: boolean; + addonConfig?: React.ReactNode; dom: Element | null; asyncEntityMetadata: AsyncState; asyncTechDocsMetadata: AsyncState; @@ -43,21 +46,29 @@ export type TechDocsReaderPageProps = { * @public */ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { - const { asyncEntityMetadata, asyncTechDocsMetadata, dom } = props; + const { + addonConfig, + asyncEntityMetadata, + asyncTechDocsMetadata, + dom, + hideHeader = false, + } = props; const { namespace, kind, name } = useParams(); const entityName = { namespace, kind, name }; return ( - - - - - {/* todo(backstage/techdocs-core): handle state indicator */} - {/* */} - - - + + + + {!hideHeader && } + + {/* todo(backstage/techdocs-core): handle state indicator */} + {/* */} + + + + ); diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index ecab23e225..0af87a2838 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -16,6 +16,7 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; @@ -89,7 +90,7 @@ export type DocsTableRow = { }; // @public -export const EmbeddedDocsRouter: () => JSX.Element; +export const EmbeddedDocsRouter: (props: PropsWithChildren<{}>) => JSX.Element; // @public export const EntityListDocsGrid: () => JSX.Element; @@ -129,7 +130,9 @@ export type EntityListDocsTableProps = { }; // @public -export const EntityTechdocsContent: () => JSX.Element; +export const EntityTechdocsContent: (props: { + children?: ReactNode; +}) => JSX.Element; // @public export const isTechDocsAvailable: (entity: Entity) => boolean; diff --git a/plugins/techdocs/src/EntityPageDocs.tsx b/plugins/techdocs/src/EntityPageDocs.tsx index b10fba6d2e..ba64ca2c78 100644 --- a/plugins/techdocs/src/EntityPageDocs.tsx +++ b/plugins/techdocs/src/EntityPageDocs.tsx @@ -14,22 +14,90 @@ * limitations under the License. */ -import React from 'react'; -import { Entity } from '@backstage/catalog-model'; -import { Reader } from './reader'; +import React, { PropsWithChildren } from 'react'; +import { + CompoundEntityRef, + DEFAULT_NAMESPACE, + Entity, +} from '@backstage/catalog-model'; +import { + Reader, + useTechDocsReaderDom, + withTechDocsReaderProvider, +} from './reader'; import { toLowerMaybe } from './helpers'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { + configApiRef, + getComponentData, + useApi, +} from '@backstage/core-plugin-api'; +import { + TechDocsReaderPage as AddonAwareReaderPage, + TECHDOCS_ADDONS_WRAPPER_KEY, +} from '@backstage/plugin-techdocs-addons'; +import { AsyncState } from 'react-use/lib/useAsyncFn'; +import { TechDocsEntityMetadata } from './types'; +import { techdocsApiRef } from '.'; +import useAsync from 'react-use/lib/useAsync'; + +type SpecialReaderPageProps = { + entityName: CompoundEntityRef; + asyncEntityMetadata: AsyncState; + addonConfig?: React.ReactNode; +}; + +// todo(backstage/techdocs-core): Combine with and simplify +// with the version in TechDocsReaderPage.tsx +const SpecialReaderPage = (props: SpecialReaderPageProps) => { + const techdocsApi = useApi(techdocsApiRef); + const dom = useTechDocsReaderDom(props.entityName); + const { kind, namespace, name } = props.entityName; + + const asyncTechDocsMetadata = useAsync(() => { + return techdocsApi.getTechDocsMetadata({ kind, namespace, name }); + }, [kind, namespace, name, techdocsApi]); -export const EntityPageDocs = ({ entity }: { entity: Entity }) => { - const config = useApi(configApiRef); return ( - ); }; + +export const EntityPageDocs = ({ + children, + entity, +}: PropsWithChildren<{ entity: Entity }>) => { + const config = useApi(configApiRef); + const entityName = { + namespace: toLowerMaybe( + entity.metadata.namespace ?? DEFAULT_NAMESPACE, + config, + ), + kind: toLowerMaybe(entity.kind, config), + name: toLowerMaybe(entity.metadata.name, config), + }; + + // Check if we were given a set of TechDocs addons. + if (children && getComponentData(children, TECHDOCS_ADDONS_WRAPPER_KEY)) { + const Component = withTechDocsReaderProvider(SpecialReaderPage, entityName); + return ( + + ); + } + + // Otherwise, return a version of the reader that is not addon-aware. + return ; +}; diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 3a49f6edf9..0a2c569097 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { Route, Routes } from 'react-router-dom'; @@ -55,7 +55,8 @@ export const Router = () => { * * @public */ -export const EmbeddedDocsRouter = () => { +export const EmbeddedDocsRouter = (props: PropsWithChildren<{}>) => { + const { children } = props; const { entity } = useEntity(); const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION]; @@ -66,7 +67,10 @@ export const EmbeddedDocsRouter = () => { return ( - } /> + {children}} + /> ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx index 1c041dfe97..e51f5272eb 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx @@ -47,8 +47,8 @@ export type TechDocsReaderPageRenderFunction = ({ type SpecialReaderPageProps = { entityName: CompoundEntityRef; - asyncEntityMetadata: AsyncState; - asyncTechDocsMetadata: AsyncState; + asyncEntityMetadata: AsyncState; + asyncTechDocsMetadata: AsyncState; }; const SpecialReaderPage = (props: SpecialReaderPageProps) => {