Skip to content

Commit

Permalink
feat(app) RHIDP-2338 expose dynamic UI config
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
gashcrumb committed May 30, 2024
1 parent 893340f commit ab0ac9f
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 88 deletions.
141 changes: 73 additions & 68 deletions packages/app/src/components/DynamicRoot/DynamicRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -33,8 +35,6 @@ export type StaticPlugins = Record<
}
>;

type EntityTabMap = Record<string, { title: string; mountPoint: string }>;

export const DynamicRoot = ({
afterInit,
apis: staticApis,
Expand All @@ -55,7 +55,7 @@ export const DynamicRoot = ({
>(undefined);
// registry of remote components loaded at bootstrap
const [components, setComponents] = useState<ComponentRegistry | undefined>();
const { initialized, pluginStore } = useScalprum();
const { initialized, pluginStore, api: scalprumApi } = useScalprum();

const themes = useThemes();

Expand Down Expand Up @@ -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<MountPoints>(
(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<DynamicRoute[]>(
(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<EntityTabOverrides>(
(acc, { path, title, mountPoint, scope }) => {
if (acc[path]) {
// eslint-disable-next-line no-console
console.warn(
Expand All @@ -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) {
Expand All @@ -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(),
Expand All @@ -333,6 +337,7 @@ export const DynamicRoot = ({
});
}, [
afterInit,
scalprumApi,
dynamicPlugins,
pluginStore,
scalprumConfig,
Expand Down
51 changes: 32 additions & 19 deletions packages/app/src/components/DynamicRoot/DynamicRootContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ export type DynamicModuleEntry = Pick<
ScalprumComponentProps,
'scope' | 'module'
>;
export type DynamicRootContextValue = DynamicModuleEntry & {
path: string;
menuItem?: MenuItem;
Component: React.ComponentType<any>;
staticJSXContent?: React.ReactNode;
config: {
props?: Record<string, any>;
};
};

type ScalprumMountPointConfigBase = {
layout?: Record<string, string>;
Expand Down Expand Up @@ -74,19 +65,41 @@ export type RemotePlugins = {
};
};

export type DynamicRoute = DynamicModuleEntry & {
path: string;
menuItem?: MenuItem;
Component: React.ComponentType<any>;
staticJSXContent?: React.ReactNode;
config: {
props?: Record<string, any>;
};
};

export type EntityTabOverrides = Record<
string,
{ title: string; mountPoint: string }
>;

export type MountPoints = Record<string, ScalprumMountPoint[]>;

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<React.PropsWithChildren>;
AppRouter: React.ComponentType<React.PropsWithChildren>;
dynamicRoutes: DynamicRootContextValue[];
entityTabOverrides: Record<string, { title: string; mountPoint: string }>;
mountPoints: { [mountPoint: string]: ScalprumMountPoint[] };
scaffolderFieldExtensions: {
scope: string;
module: string;
importName: string;
Component: React.ComponentType<{}>;
}[];
};
} & DynamicRootConfig;

const DynamicRootContext = createContext<ComponentRegistry>({
AppProvider: () => null,
Expand Down
16 changes: 15 additions & 1 deletion packages/app/src/components/DynamicRoot/ScalprumRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -65,8 +70,17 @@ const ScalprumRoot = ({
return <Loader />;
}
const { dynamicPlugins, baseUrl, scalprumConfig } = value || {};
const scalprumApiHolder = {
dynamicRootConfig: {
dynamicRoutes: [],
entityTabOverrides: {},
mountPoints: {},
scaffolderFieldExtensions: [],
} as DynamicRootConfig,
};
return (
<ScalprumProvider
<ScalprumProvider<ScalprumApiHolder>
api={scalprumApiHolder}
config={scalprumConfig ?? {}}
pluginSDKOptions={{
pluginLoaderOptions: {
Expand Down

0 comments on commit ab0ac9f

Please sign in to comment.