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) => {