Allow addons (except header location) to be used in Entity Reader

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-03-21 21:57:04 +01:00
committed by Emma Indal
parent 413024e182
commit ace749b785
9 changed files with 241 additions and 37 deletions
+58
View File
@@ -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';
// ...
<Route path="/docs" element={<TechDocsIndexPage />} />
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
- {techDocsPage}
+ <TechDocsAddons>
+ <SomeAddon />
+ </TechDocsAddons>
</Route>
// ...
```
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';
// ...
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
- <EntityTechDocsContent />
+ <EntityTechdocsContent>
+ <TechDocsAddons>
+ <SomeAddon />
+ </TechDocsAddons>
+ </EntityTechdocsContent>
</EntityLayout.Route>
</EntityLayoutWrapper>
// ...
```
If you do not wish to customize your TechDocs reader experience in this way at this time, no changes are necessary!
@@ -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 = (
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
<EntityTechdocsContent>
<TechDocsAddons>
<ExampleHeader />
<ExampleSubHeader />
<ExamplePrimarySidebar />
<ExampleSecondarySidebar />
<ExampleContent />
</TechDocsAddons>
</EntityTechdocsContent>
</EntityLayout.Route>
<EntityLayout.Route
@@ -464,7 +480,15 @@ const websiteEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
<EntityTechdocsContent>
<TechDocsAddons>
<ExampleHeader />
<ExampleSubHeader />
<ExamplePrimarySidebar />
<ExampleSecondarySidebar />
<ExampleContent />
</TechDocsAddons>
</EntityTechdocsContent>
</EntityLayout.Route>
<EntityLayout.Route
if={isNewRelicDashboardAvailable}
@@ -503,7 +527,15 @@ const defaultEntityPage = (
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
<EntityTechdocsContent>
<TechDocsAddons>
<ExampleHeader />
<ExampleSubHeader />
<ExamplePrimarySidebar />
<ExampleSecondarySidebar />
<ExampleContent />
</TechDocsAddons>
</EntityTechdocsContent>
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
+2 -2
View File
@@ -3,8 +3,6 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
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<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
+30 -2
View File
@@ -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<TechDocsAddonConfig>({});
export const TechDocsAddonConfigProvider = (
props: PropsWithChildren<{ config?: React.ReactNode }>,
) => {
const fromOutlet = useOutlet();
const config = props.config ?? fromOutlet;
return (
<TechDocsAddonConfigContext.Provider value={{ config }}>
{props.children}
</TechDocsAddonConfigContext.Provider>
);
};
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);
@@ -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<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
@@ -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 (
<TechDocsMetadataProvider asyncValue={asyncTechDocsMetadata}>
<TechDocsEntityProvider asyncValue={asyncEntityMetadata}>
<TechDocsReaderPageProvider entityName={entityName}>
<Page themeId="documentation">
<TechDocsReaderPageHeader />
<TechDocsReaderPageSubheader />
{/* todo(backstage/techdocs-core): handle state indicator */}
{/* <TechDocReaderPageIndicator /> */}
<TechDocsReaderPageContent dom={dom} />
</Page>
</TechDocsReaderPageProvider>
<TechDocsAddonConfigProvider config={addonConfig}>
<TechDocsReaderPageProvider entityName={entityName}>
<Page themeId="documentation">
{!hideHeader && <TechDocsReaderPageHeader />}
<TechDocsReaderPageSubheader />
{/* todo(backstage/techdocs-core): handle state indicator */}
{/* <TechDocReaderPageIndicator /> */}
<TechDocsReaderPageContent dom={dom} />
</Page>
</TechDocsReaderPageProvider>
</TechDocsAddonConfigProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
+5 -2
View File
@@ -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;
+81 -13
View File
@@ -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<TechDocsEntityMetadata>;
addonConfig?: React.ReactNode;
};
// todo(backstage/techdocs-core): Combine with <SpecialReaderPage> 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 (
<Reader
withSearch={false}
entityRef={{
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default', config),
kind: toLowerMaybe(entity.kind, config),
name: toLowerMaybe(entity.metadata.name, config),
}}
<AddonAwareReaderPage
asyncEntityMetadata={props.asyncEntityMetadata}
asyncTechDocsMetadata={asyncTechDocsMetadata}
addonConfig={props.addonConfig}
dom={dom}
hideHeader
/>
);
};
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 (
<Component
entityName={entityName}
asyncEntityMetadata={{
loading: false,
error: undefined,
value: entity,
}}
addonConfig={children}
/>
);
}
// Otherwise, return a version of the reader that is not addon-aware.
return <Reader withSearch={false} entityRef={entityName} />;
};
+7 -3
View File
@@ -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 (
<Routes>
<Route path="/*" element={<EntityPageDocs entity={entity} />} />
<Route
path="/*"
element={<EntityPageDocs entity={entity}>{children}</EntityPageDocs>}
/>
</Routes>
);
};
@@ -47,8 +47,8 @@ export type TechDocsReaderPageRenderFunction = ({
type SpecialReaderPageProps = {
entityName: CompoundEntityRef;
asyncEntityMetadata: AsyncState<any>;
asyncTechDocsMetadata: AsyncState<any>;
asyncEntityMetadata: AsyncState<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
};
const SpecialReaderPage = (props: SpecialReaderPageProps) => {