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';