diff --git a/packages/app/package.json b/packages/app/package.json index 4522cabaf1..69afcc11e0 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -52,6 +52,7 @@ "@backstage/plugin-shortcuts": "^0.2.5-next.0", "@backstage/plugin-tech-radar": "^0.5.11-next.1", "@backstage/plugin-techdocs": "^1.0.1-next.1", + "@backstage/plugin-techdocs-addons": "^0.0.0", "@backstage/plugin-todo": "^0.2.6-next.0", "@backstage/plugin-user-settings": "^0.4.3-next.0", "@backstage/plugin-tech-insights": "^0.1.14-next.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9cbbcc86a9..9073af7d2e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -66,14 +66,15 @@ import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { TechDocsIndexPage, - techdocsPlugin, TechDocsReaderPage, + techdocsPlugin, } from '@backstage/plugin-techdocs'; import { UserSettingsPage, UserSettingsTab, } from '@backstage/plugin-user-settings'; import { AdvancedSettings } from './components/advancedSettings'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-addons'; import AlarmIcon from '@material-ui/icons/Alarm'; import React from 'react'; import { hot } from 'react-hot-loader/root'; @@ -87,10 +88,18 @@ import { defaultPreviewTemplate } from './components/scaffolder/defaultPreviewTe import { searchPage } from './components/search/SearchPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; -import { techDocsPage } from './components/techdocs/TechDocsPage'; + +// import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { + ExampleContent, + ExampleHeader, + ExamplePrimarySidebar, + ExampleSecondarySidebar, + ExampleSubHeader, +} from './components/techdocs/ExampleAddons'; const app = createApp({ apis, @@ -177,7 +186,13 @@ const routes = ( path="/docs/:namespace/:kind/:name/*" element={} > - {techDocsPage} + + + + + + + { + return ; + }, + }), +); + +export const ExampleSubHeader = techdocsPlugin.provide( + createTechDocsAddon({ + name: 'ExampleSubHeader', + location: TechDocsAddonLocations.SUBHEADER, + component: () => { + return ( + + Subheader. + + ); + }, + }), +); + +export const ExamplePrimarySidebar = techdocsPlugin.provide( + createTechDocsAddon({ + name: 'ExamplePrimarySidebar', + location: TechDocsAddonLocations.PRIMARY_SIDEBAR, + component: () => { + return ( + + Primary Sidebar. + + ); + }, + }), +); + +export const ExampleSecondarySidebar = techdocsPlugin.provide( + createTechDocsAddon({ + name: 'ExampleSecondarySidebar', + location: TechDocsAddonLocations.SECONDARY_SIDEBAR, + component: () => { + return ( + + Secondary Sidebar. + + ); + }, + }), +); + +const ExampleContentComponent = () => { + const h1 = useShadowRootElements(['h1'])[0]; + useEffect(() => { + if (h1 && !h1.innerText.startsWith('Modified: ')) { + h1.innerText = `Modified: ${h1.innerText}`; + } + }, [h1]); + return null; +}; + +export const ExampleContent = techdocsPlugin.provide( + createTechDocsAddon({ + name: 'ExampleContent', + location: TechDocsAddonLocations.CONTENT, + component: ExampleContentComponent, + }), +); diff --git a/plugins/techdocs-addons/api-report.md b/plugins/techdocs-addons/api-report.md index 26c0825493..5f7f2e8cd0 100644 --- a/plugins/techdocs-addons/api-report.md +++ b/plugins/techdocs-addons/api-report.md @@ -7,17 +7,18 @@ import { AsyncState } from 'react-use/lib/useAsyncFn'; import { ComponentType } from 'react'; -import { CompoundEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { Extension } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; -import { TechDocsEntityMetadata } from '@backstage/plugin-techdocs'; -import { TechDocsMetadata } from '@backstage/plugin-techdocs'; // @public export function createTechDocsAddon( options: TechDocsAddonOptions, ): Extension>; +// @public +export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1'; + // @public export type TechDocsAddonAsyncMetadata = AsyncState; @@ -41,6 +42,20 @@ export type TechDocsAddonOptions = { // @public export const TechDocsAddons: React_2.ComponentType; +// @public +export type TechDocsEntityMetadata = Entity & { + locationMetadata?: { + type: string; + target: string; + }; +}; + +// @public +export type TechDocsMetadata = { + site_name: string; + site_description: string; +}; + // @public export const TechDocsReaderPage: ( props: TechDocsReaderPageProps, @@ -48,7 +63,9 @@ export const TechDocsReaderPage: ( // @public (undocumented) export type TechDocsReaderPageProps = { - entityName: CompoundEntityRef; + dom: Element | null; + asyncEntityMetadata: AsyncState; + asyncTechDocsMetadata: AsyncState; }; // @public diff --git a/plugins/techdocs-addons/package.json b/plugins/techdocs-addons/package.json index 4769ebd189..1b3d736a7d 100644 --- a/plugins/techdocs-addons/package.json +++ b/plugins/techdocs-addons/package.json @@ -25,7 +25,6 @@ "@backstage/catalog-model": "^0.13.0", "@backstage/core-components": "^0.9.1", "@backstage/core-plugin-api": "^0.8.0", - "@backstage/plugin-techdocs": "^0.15.1", "@material-ui/core": "^4.12.2", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.11.0", diff --git a/plugins/techdocs-addons/src/addons.tsx b/plugins/techdocs-addons/src/addons.tsx index 06ce1eb137..afb3b151c0 100644 --- a/plugins/techdocs-addons/src/addons.tsx +++ b/plugins/techdocs-addons/src/addons.tsx @@ -27,6 +27,11 @@ import { useOutlet } from 'react-router-dom'; import { TechDocsAddonLocations, TechDocsAddonOptions } from './types'; export const TECHDOCS_ADDONS_KEY = 'techdocs.addons.addon.v1'; + +/** + * Marks the registry component. + * @public + */ export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1'; /** diff --git a/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx index a9db8ddb6d..d9d54e2248 100644 --- a/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs-addons/src/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -14,21 +14,17 @@ * limitations under the License. */ -import { CompoundEntityRef } from '@backstage/catalog-model'; import { Page } from '@backstage/core-components'; -// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs -import { - withTechDocsReaderProvider, - // @ts-ignore - TechDocsStateIndicator as TechDocReaderPageIndicator, -} from '@backstage/plugin-techdocs'; import React from 'react'; +import { useParams } from 'react-router-dom'; +import { AsyncState } from 'react-use/lib/useAsyncFn'; import { TechDocsMetadataProvider, TechDocsEntityProvider, TechDocsReaderPageProvider, } from '../../context'; +import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent'; import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader'; import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; @@ -36,29 +32,33 @@ import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; /** * @public */ -export type TechDocsReaderPageProps = { entityName: CompoundEntityRef }; +export type TechDocsReaderPageProps = { + dom: Element | null; + asyncEntityMetadata: AsyncState; + asyncTechDocsMetadata: AsyncState; +}; /** * An addon-aware implementation of the TechDocsReaderPage. * @public */ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { - const { entityName } = props; - const Component = withTechDocsReaderProvider(() => { - return ( - - - - - - - - - - - - - ); - }, entityName); - return ; + const { asyncEntityMetadata, asyncTechDocsMetadata, dom } = props; + const { namespace, kind, name } = useParams(); + const entityName = { namespace, kind, name }; + return ( + + + + + + + {/* todo(backstage/techdocs-core): handle state indicator */} + {/* */} + + + + + + ); }; diff --git a/plugins/techdocs-addons/src/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs-addons/src/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index 9bdab3f5c5..6e3ec99b21 100644 --- a/plugins/techdocs-addons/src/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs-addons/src/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -15,9 +15,6 @@ */ import { Content, Progress } from '@backstage/core-components'; -// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs -// @ts-ignore -import { useTechDocsReaderDom } from '@backstage/plugin-techdocs'; import { Portal } from '@material-ui/core'; import { StylesProvider, jssPreset } from '@material-ui/styles'; import React, { useEffect, useRef, useState } from 'react'; @@ -27,7 +24,7 @@ import { useTechDocsAddons } from '../../addons'; import { useTechDocsReaderPage } from '../../context'; import { TechDocsAddonLocations as locations } from '../../types'; -export const TechDocsReaderPageContent = () => { +export const TechDocsReaderPageContent = ({ dom }: { dom: Element | null }) => { const ref = useRef(null); const [jss, setJss] = useState( create({ @@ -37,8 +34,7 @@ export const TechDocsReaderPageContent = () => { ); const addons = useTechDocsAddons(); - const { entityName, setShadowRoot } = useTechDocsReaderPage(); - const dom = useTechDocsReaderDom(entityName); + const { setShadowRoot } = useTechDocsReaderPage(); useEffect(() => { const shadowHost = ref.current; @@ -61,10 +57,10 @@ export const TechDocsReaderPageContent = () => { '[data-md-component="container"]', ); const primarySidebarElement = ref.current?.shadowRoot?.querySelector( - '[data-md-component="navigation"]', + 'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]', ); const secondarySidebarElement = ref.current?.shadowRoot?.querySelector( - '[data-md-component="toc"]', + 'div[data-md-component="sidebar"][data-md-type="toc"], div[data-md-component="toc"]', ); const primarySidebarAddonLocation = document.createElement('div'); diff --git a/plugins/techdocs-addons/src/context.tsx b/plugins/techdocs-addons/src/context.tsx index 9aa057117d..54a73e2589 100644 --- a/plugins/techdocs-addons/src/context.tsx +++ b/plugins/techdocs-addons/src/context.tsx @@ -15,12 +15,6 @@ */ import { CompoundEntityRef } from '@backstage/catalog-model'; -import { useApi } from '@backstage/core-plugin-api'; -import { - techdocsApiRef, - TechDocsEntityMetadata, - TechDocsMetadata, -} from '@backstage/plugin-techdocs'; import React, { createContext, Dispatch, @@ -29,9 +23,16 @@ import React, { useContext, useState, } from 'react'; -import useAsync from 'react-use/lib/useAsync'; -import { TechDocsAddonAsyncMetadata } from './types'; +import { AsyncState } from 'react-use/lib/useAsync'; +import { + TechDocsAddonAsyncMetadata, + TechDocsEntityMetadata, + TechDocsMetadata, +} from './types'; +type PropsWithAsyncMetadata = PropsWithChildren<{ + asyncValue: AsyncState; +}>; type PropsWithEntityName = PropsWithChildren<{ entityName: CompoundEntityRef }>; const initialContextValue = { @@ -46,17 +47,11 @@ const TechDocsMetadataContext = ); export const TechDocsMetadataProvider = ({ - entityName, + asyncValue, children, -}: PropsWithEntityName) => { - const techdocsApi = useApi(techdocsApiRef); - - const metadataResponse = useAsync(async () => { - return await techdocsApi.getTechDocsMetadata(entityName); - }, []); - +}: PropsWithAsyncMetadata) => { return ( - + {children} ); @@ -77,17 +72,11 @@ const TechDocsEntityContext = ); export const TechDocsEntityProvider = ({ - entityName, + asyncValue, children, -}: PropsWithEntityName) => { - const techdocsApi = useApi(techdocsApiRef); - - const metadataResponse = useAsync(async () => { - return await techdocsApi.getEntityMetadata(entityName); - }, []); - +}: PropsWithAsyncMetadata) => { return ( - + {children} ); diff --git a/plugins/techdocs-addons/src/index.ts b/plugins/techdocs-addons/src/index.ts index 0993eaba97..0d966cfd67 100644 --- a/plugins/techdocs-addons/src/index.ts +++ b/plugins/techdocs-addons/src/index.ts @@ -20,12 +20,17 @@ * @packageDocumentation */ -export { createTechDocsAddon, TechDocsAddons } from './addons'; +export { + createTechDocsAddon, + TechDocsAddons, + TECHDOCS_ADDONS_WRAPPER_KEY, +} from './addons'; export * from './components'; export { useEntityMetadata, useTechDocsMetadata } from './context'; -export { useShadowRoot, useShadowRootElements } from './hooks'; +export { TechDocsAddonLocations } from './types'; export type { TechDocsAddonAsyncMetadata, - TechDocsAddonLocations, TechDocsAddonOptions, + TechDocsMetadata, + TechDocsEntityMetadata, } from './types'; diff --git a/plugins/techdocs-addons/src/types.ts b/plugins/techdocs-addons/src/types.ts index 8e42e0a6bc..d8ab7e9520 100644 --- a/plugins/techdocs-addons/src/types.ts +++ b/plugins/techdocs-addons/src/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import { ComponentType } from 'react'; import { AsyncState } from 'react-use/lib/useAsyncFn'; @@ -77,3 +78,22 @@ export type TechDocsAddonOptions = { * @public */ export type TechDocsAddonAsyncMetadata = AsyncState; + +/** + * Metadata for TechDocs page + * + * @public + */ +export type TechDocsMetadata = { + site_name: string; + site_description: string; +}; + +/** + * Metadata for TechDocs Entity + * + * @public + */ +export type TechDocsEntityMetadata = Entity & { + locationMetadata?: { type: string; target: string }; +}; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d0ef75e32c..b4868ab4e7 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -43,7 +43,9 @@ "@backstage/integration": "^1.1.0-next.1", "@backstage/integration-react": "^1.0.1-next.1", "@backstage/plugin-catalog-react": "^1.0.1-next.2", + "@backstage/plugin-catalog": "^0.10.0", "@backstage/plugin-search": "^0.7.5-next.0", + "@backstage/plugin-techdocs-addons": "^0.0.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx index 1bfd0e6f51..1c041dfe97 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx @@ -17,14 +17,17 @@ import React, { useCallback, useState } from 'react'; import { useOutlet } from 'react-router'; import { useParams } from 'react-router-dom'; -import useAsync from 'react-use/lib/useAsync'; -import { Reader } from './Reader'; -import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader'; +import useAsync, { AsyncState } from 'react-use/lib/useAsync'; import { techdocsApiRef } from '../../api'; import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; import { CompoundEntityRef } from '@backstage/catalog-model'; -import { useApi, useApp } from '@backstage/core-plugin-api'; -import { Page, Content } from '@backstage/core-components'; +import { getComponentData, useApi, useApp } from '@backstage/core-plugin-api'; +import { Page } from '@backstage/core-components'; +import { + TechDocsReaderPage as AddonAwareReaderPage, + TECHDOCS_ADDONS_WRAPPER_KEY, +} from '@backstage/plugin-techdocs-addons'; +import { useTechDocsReaderDom, withTechDocsReaderProvider } from './Reader'; /** * Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata @@ -42,6 +45,24 @@ export type TechDocsReaderPageRenderFunction = ({ onReady: () => void; }) => JSX.Element; +type SpecialReaderPageProps = { + entityName: CompoundEntityRef; + asyncEntityMetadata: AsyncState; + asyncTechDocsMetadata: AsyncState; +}; + +const SpecialReaderPage = (props: SpecialReaderPageProps) => { + const dom = useTechDocsReaderDom(props.entityName); + + return ( + + ); +}; + /** * Props for {@link TechDocsReaderPage} * @@ -61,7 +82,7 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { const techdocsApi = useApi(techdocsApiRef); - const { value: techdocsMetadataValue } = useAsync(() => { + const asyncTechDocsMetadata = useAsync(() => { if (documentReady) { return techdocsApi.getTechDocsMetadata({ kind, namespace, name }); } @@ -69,50 +90,47 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { 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 asyncEntityMetadata = useAsync(() => { + return techdocsApi.getEntityMetadata({ kind, namespace, name }); + }, [kind, namespace, name, techdocsApi]); const onReady = useCallback(() => { setDocumentReady(true); }, [setDocumentReady]); - if (entityMetadataError) return ; + if (asyncEntityMetadata.error) return ; - if (!children) - return ( - outlet || ( - - - - - - - ) - ); + ); + } + // Otherwise, just return the outlet (legacy-style composability). + return outlet; + } + } return ( {children instanceof Function ? children({ - techdocsMetadataValue, - entityMetadataValue, + techdocsMetadataValue: asyncTechDocsMetadata.value, + entityMetadataValue: asyncEntityMetadata.value, entityRef: { kind, namespace, name }, onReady, })