Testbed to help iterate on addons + correct dependency graph

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-03-21 12:26:03 +01:00
committed by Emma Indal
parent 0b67af3485
commit 10a7736ced
13 changed files with 280 additions and 109 deletions
+1
View File
@@ -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",
+18 -3
View File
@@ -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={<TechDocsReaderPage />}
>
{techDocsPage}
<TechDocsAddons>
<ExampleHeader />
<ExampleSubHeader />
<ExamplePrimarySidebar />
<ExampleSecondarySidebar />
<ExampleContent />
</TechDocsAddons>
</Route>
<Route
path="/create"
@@ -0,0 +1,104 @@
/*
* Copyright 2022 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 { HeaderLabel } from '@backstage/core-components';
import { techdocsPlugin } from '@backstage/plugin-techdocs';
import {
createTechDocsAddon,
TechDocsAddonLocations,
useShadowRootElements,
} from '@backstage/plugin-techdocs-addons';
import { Card, CardContent } from '@material-ui/core';
import React, { useEffect } from 'react';
/**
* Note: this is not typically how or where one might define such things. It
* would more typically be exported/provided by a plugin!
*
* In fact, this whole file and usage should be deleted before we merge things
* in. This is just a convenient way to test the addon framework in a nice
* end-to-end way before releasing anything.
*/
export const ExampleHeader = techdocsPlugin.provide(
createTechDocsAddon({
name: 'ExampleHeader',
location: TechDocsAddonLocations.HEADER,
component: () => {
return <HeaderLabel label="Label" value="Value" />;
},
}),
);
export const ExampleSubHeader = techdocsPlugin.provide(
createTechDocsAddon({
name: 'ExampleSubHeader',
location: TechDocsAddonLocations.SUBHEADER,
component: () => {
return (
<Card>
<CardContent>Subheader.</CardContent>
</Card>
);
},
}),
);
export const ExamplePrimarySidebar = techdocsPlugin.provide(
createTechDocsAddon({
name: 'ExamplePrimarySidebar',
location: TechDocsAddonLocations.PRIMARY_SIDEBAR,
component: () => {
return (
<Card>
<CardContent>Primary Sidebar.</CardContent>
</Card>
);
},
}),
);
export const ExampleSecondarySidebar = techdocsPlugin.provide(
createTechDocsAddon({
name: 'ExampleSecondarySidebar',
location: TechDocsAddonLocations.SECONDARY_SIDEBAR,
component: () => {
return (
<Card>
<CardContent>Secondary Sidebar.</CardContent>
</Card>
);
},
}),
);
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,
}),
);
+21 -4
View File
@@ -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<TComponentProps>(
options: TechDocsAddonOptions<TComponentProps>,
): Extension<ComponentType<TComponentProps>>;
// @public
export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1';
// @public
export type TechDocsAddonAsyncMetadata<TValue> = AsyncState<TValue | undefined>;
@@ -41,6 +42,20 @@ export type TechDocsAddonOptions<TAddonProps = {}> = {
// @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<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
};
// @public
-1
View File
@@ -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",
+5
View File
@@ -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 <TechDocsAddons> registry component.
* @public
*/
export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1';
/**
@@ -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<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
};
/**
* An addon-aware implementation of the TechDocsReaderPage.
* @public
*/
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
const { entityName } = props;
const Component = withTechDocsReaderProvider(() => {
return (
<TechDocsMetadataProvider entityName={entityName}>
<TechDocsEntityProvider entityName={entityName}>
<TechDocsReaderPageProvider entityName={entityName}>
<Page themeId="documentation">
<TechDocsReaderPageHeader />
<TechDocsReaderPageSubheader />
<TechDocReaderPageIndicator />
<TechDocsReaderPageContent />
</Page>
</TechDocsReaderPageProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
}, entityName);
return <Component />;
const { asyncEntityMetadata, asyncTechDocsMetadata, dom } = 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>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
};
@@ -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<HTMLDivElement>(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');
+15 -26
View File
@@ -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<TMetadata> = PropsWithChildren<{
asyncValue: AsyncState<TMetadata>;
}>;
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<TechDocsMetadata>) => {
return (
<TechDocsMetadataContext.Provider value={metadataResponse}>
<TechDocsMetadataContext.Provider value={asyncValue}>
{children}
</TechDocsMetadataContext.Provider>
);
@@ -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<TechDocsEntityMetadata>) => {
return (
<TechDocsEntityContext.Provider value={metadataResponse}>
<TechDocsEntityContext.Provider value={asyncValue}>
{children}
</TechDocsEntityContext.Provider>
);
+8 -3
View File
@@ -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';
+20
View File
@@ -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<TAddonProps = {}> = {
* @public
*/
export type TechDocsAddonAsyncMetadata<TValue> = AsyncState<TValue | undefined>;
/**
* 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 };
};
+2
View File
@@ -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",
@@ -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<any>;
asyncTechDocsMetadata: AsyncState<any>;
};
const SpecialReaderPage = (props: SpecialReaderPageProps) => {
const dom = useTechDocsReaderDom(props.entityName);
return (
<AddonAwareReaderPage
asyncEntityMetadata={props.asyncEntityMetadata}
asyncTechDocsMetadata={props.asyncTechDocsMetadata}
dom={dom}
/>
);
};
/**
* 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 <NotFoundErrorPage />;
if (asyncEntityMetadata.error) return <NotFoundErrorPage />;
if (!children)
return (
outlet || (
<Page themeId="documentation">
<TechDocsReaderPageHeader
techDocsMetadata={techdocsMetadataValue}
entityMetadata={entityMetadataValue}
entityRef={{
kind,
namespace,
name,
}}
if (!children) {
if (outlet) {
// If the outlet is a single child and that child is an instance of the
// TechDocsAddons registry, then render it a certain way.
if (
getComponentData(outlet.props.children, TECHDOCS_ADDONS_WRAPPER_KEY)
) {
const Component = withTechDocsReaderProvider(SpecialReaderPage, {
kind,
namespace,
name,
});
return (
<Component
entityName={{ kind, namespace, name }}
asyncEntityMetadata={asyncEntityMetadata}
asyncTechDocsMetadata={asyncTechDocsMetadata}
/>
<Content data-testid="techdocs-content">
<Reader
onReady={onReady}
entityRef={{
kind,
namespace,
name,
}}
/>
</Content>
</Page>
)
);
);
}
// Otherwise, just return the outlet (legacy-style composability).
return outlet;
}
}
return (
<Page themeId="documentation">
{children instanceof Function
? children({
techdocsMetadataValue,
entityMetadataValue,
techdocsMetadataValue: asyncTechDocsMetadata.value,
entityMetadataValue: asyncEntityMetadata.value,
entityRef: { kind, namespace, name },
onReady,
})