Allow addons (except header location) to be used in Entity Reader
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
committed by
Emma Indal
parent
413024e182
commit
ace749b785
@@ -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">
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user