From ab0ac9f3e65f9b6374426d06a1a344ebe70f1efa Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Thu, 16 May 2024 14:01:54 -0400 Subject: [PATCH] feat(app) RHIDP-2338 expose dynamic UI config This change exposes the dynamic UI configuration to dynamic plugins via the scalprum API holder available with the scalprum React API. This change also moves around some blocks for consistency and improves the typing for the DynamicRootContext objects. Signed-off-by: Stan Lewis --- .../components/DynamicRoot/DynamicRoot.tsx | 141 +++++++++--------- .../DynamicRoot/DynamicRootContext.tsx | 51 ++++--- .../components/DynamicRoot/ScalprumRoot.tsx | 16 +- 3 files changed, 120 insertions(+), 88 deletions(-) diff --git a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx index 3a7548d33..3fe1eb082 100644 --- a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx +++ b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx @@ -5,13 +5,15 @@ import { BackstageApp } from '@backstage/core-app-api'; import { AnyApiFactory, BackstagePlugin } from '@backstage/core-plugin-api'; import { useThemes } from '@redhat-developer/red-hat-developer-hub-theme'; -import { AppsConfig, getScalprum } from '@scalprum/core'; +import { AppsConfig } from '@scalprum/core'; import { useScalprum } from '@scalprum/react-core'; import DynamicRootContext, { ComponentRegistry, - DynamicRootContextValue, + DynamicRoute, + EntityTabOverrides, + MountPoints, RemotePlugins, - ScalprumMountPoint, + ScaffolderFieldExtension, ScalprumMountPointConfig, } from './DynamicRootContext'; import extractDynamicConfig, { @@ -33,8 +35,6 @@ export type StaticPlugins = Record< } >; -type EntityTabMap = Record; - export const DynamicRoot = ({ afterInit, apis: staticApis, @@ -55,7 +55,7 @@ export const DynamicRoot = ({ >(undefined); // registry of remote components loaded at bootstrap const [components, setComponents] = useState(); - const { initialized, pluginStore } = useScalprum(); + const { initialized, pluginStore, api: scalprumApi } = useScalprum(); const themes = useThemes(); @@ -224,51 +224,51 @@ export const DynamicRoot = ({ return acc; }, []); - const mountPointComponents = providerMountPoints.reduce<{ - [mountPoint: string]: ScalprumMountPoint[]; - }>((acc, entry) => { - if (!acc[entry.mountPoint]) { - acc[entry.mountPoint] = []; - } - acc[entry.mountPoint].push({ - Component: entry.Component, - staticJSXContent: entry.staticJSXContent, - config: entry.config, - }); - return acc; - }, {}); - - getScalprum().api.mountPoints = mountPointComponents; - - const dynamicRoutesComponents = dynamicRoutes.reduce< - DynamicRootContextValue[] - >((acc, route) => { - const Component = - allPlugins[route.scope]?.[route.module]?.[route.importName]; - if (Component) { - acc.push({ - ...route, - Component: - typeof Component === 'object' && 'element' in Component - ? (Component.element as React.ComponentType<{}>) - : (Component as React.ComponentType<{}>), - staticJSXContent: - typeof Component === 'object' && 'staticJSXContent' in Component - ? (Component.staticJSXContent as React.ReactNode) - : null, - config: route.config ?? {}, + const mountPointComponents = providerMountPoints.reduce( + (acc, entry) => { + if (!acc[entry.mountPoint]) { + acc[entry.mountPoint] = []; + } + acc[entry.mountPoint].push({ + Component: entry.Component, + staticJSXContent: entry.staticJSXContent, + config: entry.config, }); - } else { - // eslint-disable-next-line no-console - console.warn( - `Plugin ${route.scope} is not configured properly: ${route.module}.${route.importName} not found, ignoring dynamicRoute: "${route.path}"`, - ); - } - return acc; - }, []); + return acc; + }, + {}, + ); + + const dynamicRoutesComponents = dynamicRoutes.reduce( + (acc, route) => { + const Component = + allPlugins[route.scope]?.[route.module]?.[route.importName]; + if (Component) { + acc.push({ + ...route, + Component: + typeof Component === 'object' && 'element' in Component + ? (Component.element as React.ComponentType<{}>) + : (Component as React.ComponentType<{}>), + staticJSXContent: + typeof Component === 'object' && 'staticJSXContent' in Component + ? (Component.staticJSXContent as React.ReactNode) + : null, + config: route.config ?? {}, + }); + } else { + // eslint-disable-next-line no-console + console.warn( + `Plugin ${route.scope} is not configured properly: ${route.module}.${route.importName} not found, ignoring dynamicRoute: "${route.path}"`, + ); + } + return acc; + }, + [], + ); - const entityTabOverrides: EntityTabMap = entityTabs.reduce( - (acc: EntityTabMap, { path, title, mountPoint, scope }) => { + const entityTabOverrides = entityTabs.reduce( + (acc, { path, title, mountPoint, scope }) => { if (acc[path]) { // eslint-disable-next-line no-console console.warn( @@ -279,28 +279,11 @@ export const DynamicRoot = ({ } return acc; }, - {} as EntityTabMap, + {}, ); - if (!app.current) { - app.current = createApp({ - apis: [...staticApis, ...remoteApis], - bindRoutes({ bind }) { - bindAppRoutes(bind, resolvedRouteBindingTargets, routeBindings); - }, - icons, - plugins: Object.values(staticPluginStore).map(entry => entry.plugin), - themes, - components: defaultAppComponents, - }); - } const scaffolderFieldExtensionComponents = scaffolderFieldExtensions.reduce< - { - scope: string; - module: string; - importName: string; - Component: React.ComponentType<{}>; - }[] + ScaffolderFieldExtension[] >((acc, { scope, module, importName }) => { const extensionComponent = allPlugins[scope]?.[module]?.[importName]; if (extensionComponent) { @@ -319,6 +302,27 @@ export const DynamicRoot = ({ return acc; }, []); + if (!app.current) { + app.current = createApp({ + apis: [...staticApis, ...remoteApis], + bindRoutes({ bind }) { + bindAppRoutes(bind, resolvedRouteBindingTargets, routeBindings); + }, + icons, + plugins: Object.values(staticPluginStore).map(entry => entry.plugin), + themes, + components: defaultAppComponents, + }); + } + + // make the dynamic UI configuration available to plugins + scalprumApi!.dynamicRootConfig.dynamicRoutes = dynamicRoutesComponents; + scalprumApi!.dynamicRootConfig.entityTabOverrides = entityTabOverrides; + scalprumApi!.dynamicRootConfig.mountPoints = mountPointComponents; + scalprumApi!.dynamicRootConfig.scaffolderFieldExtensions = + scaffolderFieldExtensionComponents; + + // make the dynamic UI configuration available to DynamicRootContext consumers setComponents({ AppProvider: app.current.getProvider(), AppRouter: app.current.getRouter(), @@ -333,6 +337,7 @@ export const DynamicRoot = ({ }); }, [ afterInit, + scalprumApi, dynamicPlugins, pluginStore, scalprumConfig, diff --git a/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx b/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx index d810ffbb8..4b704dac2 100644 --- a/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx +++ b/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx @@ -20,15 +20,6 @@ export type DynamicModuleEntry = Pick< ScalprumComponentProps, 'scope' | 'module' >; -export type DynamicRootContextValue = DynamicModuleEntry & { - path: string; - menuItem?: MenuItem; - Component: React.ComponentType; - staticJSXContent?: React.ReactNode; - config: { - props?: Record; - }; -}; type ScalprumMountPointConfigBase = { layout?: Record; @@ -74,19 +65,41 @@ export type RemotePlugins = { }; }; +export type DynamicRoute = DynamicModuleEntry & { + path: string; + menuItem?: MenuItem; + Component: React.ComponentType; + staticJSXContent?: React.ReactNode; + config: { + props?: Record; + }; +}; + +export type EntityTabOverrides = Record< + string, + { title: string; mountPoint: string } +>; + +export type MountPoints = Record; + +export type ScaffolderFieldExtension = { + scope: string; + module: string; + importName: string; + Component: React.ComponentType<{}>; +}; + +export type DynamicRootConfig = { + dynamicRoutes: DynamicRoute[]; + entityTabOverrides: EntityTabOverrides; + mountPoints: MountPoints; + scaffolderFieldExtensions: ScaffolderFieldExtension[]; +}; + export type ComponentRegistry = { AppProvider: React.ComponentType; AppRouter: React.ComponentType; - dynamicRoutes: DynamicRootContextValue[]; - entityTabOverrides: Record; - mountPoints: { [mountPoint: string]: ScalprumMountPoint[] }; - scaffolderFieldExtensions: { - scope: string; - module: string; - importName: string; - Component: React.ComponentType<{}>; - }[]; -}; +} & DynamicRootConfig; const DynamicRootContext = createContext({ AppProvider: () => null, diff --git a/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx b/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx index 587067b56..0f144ff02 100644 --- a/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx +++ b/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx @@ -13,6 +13,11 @@ import Loader from './Loader'; import { AppConfig } from '@backstage/config'; import { DynamicRoot, StaticPlugins } from './DynamicRoot'; import { DynamicPluginConfig } from '../../utils/dynamicUI/extractDynamicConfig'; +import { DynamicRootConfig } from './DynamicRootContext'; + +export type ScalprumApiHolder = { + dynamicRootConfig: DynamicRootConfig; +}; const ScalprumRoot = ({ apis, @@ -65,8 +70,17 @@ const ScalprumRoot = ({ return ; } const { dynamicPlugins, baseUrl, scalprumConfig } = value || {}; + const scalprumApiHolder = { + dynamicRootConfig: { + dynamicRoutes: [], + entityTabOverrides: {}, + mountPoints: {}, + scaffolderFieldExtensions: [], + } as DynamicRootConfig, + }; return ( - + api={scalprumApiHolder} config={scalprumConfig ?? {}} pluginSDKOptions={{ pluginLoaderOptions: {