diff --git a/packages/app/src/components/DynamicRoot/DynamicRoot.test.tsx b/packages/app/src/components/DynamicRoot/DynamicRoot.test.tsx index 62ad243bb0..12650eee20 100644 --- a/packages/app/src/components/DynamicRoot/DynamicRoot.test.tsx +++ b/packages/app/src/components/DynamicRoot/DynamicRoot.test.tsx @@ -16,7 +16,6 @@ import { import { Entity } from '@backstage/catalog-model'; import * as appDefaults from '@backstage/app-defaults'; import { AppRouteBinder, defaultConfigLoader } from '@backstage/core-app-api'; -import { AppConfig } from '@backstage/config'; const DynamicRoot = React.lazy(() => import('./DynamicRoot')); @@ -68,9 +67,11 @@ const MockPage = () => { ); }; -const loadAppConfig = async () => await defaultConfigLoader(); - -const MockApp = ({ appConfig }: { appConfig: AppConfig[] }) => ( +const MockApp = ({ + dynamicPlugins, +}: { + dynamicPlugins: any; // allow tests to supply specific values for specific use cases +}) => ( ( }, }) } - appConfig={appConfig} - baseFrontendConfig={{ - context: '', - data: {}, - }} + dynamicPlugins={dynamicPlugins} scalprumConfig={{}} /> @@ -124,25 +121,26 @@ jest.mock('../../utils/dynamicUI/initializeRemotePlugins', () => ({ __esModule: true, })); -const mockProcessEnv = (dynamicPluginsConfig: { [key: string]: any }) => ({ - NODE_ENV: 'test', - APP_CONFIG: [ - { - data: { - app: { title: 'Test' }, - backend: { baseUrl: 'http://localhost:7007' }, - techdocs: { - storageUrl: 'http://localhost:7007/api/techdocs/static/docs', - }, - auth: { environment: 'development' }, - dynamicPlugins: { - frontend: dynamicPluginsConfig, +const loadTestConfig = async (dynamicPlugins: any) => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { title: 'Test' }, + backend: { baseUrl: 'http://localhost:7007' }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + auth: { environment: 'development' }, + dynamicPlugins, }, + context: 'test', }, - context: 'test', - }, - ] as any, -}); + ] as any, + }; + await defaultConfigLoader(); +}; const consoleSpy = jest.spyOn(console, 'warn'); @@ -190,11 +188,15 @@ describe('DynamicRoot', () => { }); it('should render with one dynamicRoute', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { dynamicRoutes: [{ path: '/foo' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'foo.bar': { dynamicRoutes: [{ path: '/foo' }] }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -205,11 +207,17 @@ describe('DynamicRoot', () => { }); it('should render with two dynamicRoutes', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { dynamicRoutes: [{ path: '/foo' }, { path: '/bar' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'foo.bar': { + dynamicRoutes: [{ path: '/foo' }, { path: '/bar' }], + }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -220,11 +228,17 @@ describe('DynamicRoot', () => { }); it('should render with one dynamicRoute from nonexistent plugin', async () => { - process.env = mockProcessEnv({ - 'doesnt.exist': { dynamicRoutes: [{ path: '/foo' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'doesnt.exist': { + dynamicRoutes: [{ path: '/foo' }], + }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -238,13 +252,17 @@ describe('DynamicRoot', () => { }); it('should render with one dynamicRoute with nonexistent importName', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - dynamicRoutes: [{ path: '/foo', importName: 'BarComponent' }], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + dynamicRoutes: [{ path: '/foo', importName: 'BarComponent' }], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -258,11 +276,17 @@ describe('DynamicRoot', () => { }); it('should render with one dynamicRoute with nonexistent module', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { dynamicRoutes: [{ path: '/foo', module: 'BarPlugin' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'foo.bar': { + dynamicRoutes: [{ path: '/foo', module: 'BarPlugin' }], + }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -276,15 +300,22 @@ describe('DynamicRoot', () => { }); it('should render with one dynamicRoute with staticJSXContent', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - dynamicRoutes: [ - { path: '/foo', importName: 'FooComponentWithStaticJSX' }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + dynamicRoutes: [ + { + path: '/foo', + importName: 'FooComponentWithStaticJSX', + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -297,11 +328,21 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with single component', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { mountPoints: [{ mountPoint: 'a.b.c/cards' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + }, + ], + }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -312,16 +353,24 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with two components', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { mountPoint: 'a.b.c/cards' }, - { mountPoint: 'a.b.c/cards' }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + }, + { + mountPoint: 'a.b.c/cards', + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -332,16 +381,25 @@ describe('DynamicRoot', () => { }); it("should render with one mountPoint with two components where one importName doesn't exist", async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { mountPoint: 'a.b.c/cards' }, - { mountPoint: 'a.b.c/cards', importName: 'BarComponent' }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + }, + { + mountPoint: 'a.b.c/cards', + importName: 'BarComponent', + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -355,18 +413,22 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with config.if === true', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { - mountPoint: 'a.b.c/cards', - config: { if: { allOf: ['isFooConditionTrue'] } }, - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + config: { if: { allOf: ['isFooConditionTrue'] } }, + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -379,18 +441,22 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with config.if === false', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { - mountPoint: 'a.b.c/cards', - config: { if: { allOf: ['isFooConditionFalse'] } }, - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + config: { if: { allOf: ['isFooConditionFalse'] } }, + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -403,18 +469,22 @@ describe('DynamicRoot', () => { }); it("should render with one mountPoint with config.if where importName doesn't exist", async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { - mountPoint: 'a.b.c/cards', - config: { if: { allOf: ['isBarConditionTrue'] } }, - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + config: { if: { allOf: ['isBarConditionTrue'] } }, + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -427,16 +497,20 @@ describe('DynamicRoot', () => { }); it('should render with two mountPoints with one component each', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { mountPoint: 'a.b.c/cards' }, - { mountPoint: 'x.y.z/cards' }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { mountPoint: 'a.b.c/cards' }, + { mountPoint: 'x.y.z/cards' }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -449,11 +523,15 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint from nonexistent plugin', async () => { - process.env = mockProcessEnv({ - 'doesnt.exist': { mountPoints: [{ mountPoint: 'a.b.c/cards' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'doesnt.exist': { mountPoints: [{ mountPoint: 'a.b.c/cards' }] }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -467,15 +545,19 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with nonexistent importName', async () => { - process.env = mockProcessEnv({ - 'doesnt.exist': { - mountPoints: [ - { mountPoint: 'a.b.c/cards', importName: 'BarComponent' }, - ], + const dynamicPlugins = { + frontend: { + 'doesnt.exist': { + mountPoints: [ + { mountPoint: 'a.b.c/cards', importName: 'BarComponent' }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -489,13 +571,17 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with nonexistent module', async () => { - process.env = mockProcessEnv({ - 'doesnt.exist': { - mountPoints: [{ mountPoint: 'a.b.c/cards', module: 'BarPlugin' }], + const dynamicPlugins = { + frontend: { + 'doesnt.exist': { + mountPoints: [{ mountPoint: 'a.b.c/cards', module: 'BarPlugin' }], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -509,18 +595,22 @@ describe('DynamicRoot', () => { }); it('should render with one mountPoint with staticJSXContent', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - mountPoints: [ - { - mountPoint: 'a.b.c/cards', - importName: 'FooComponentWithStaticJSX', - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + mountPoints: [ + { + mountPoint: 'a.b.c/cards', + importName: 'FooComponentWithStaticJSX', + }, + ], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -533,11 +623,15 @@ describe('DynamicRoot', () => { }); it('should render with one appIcon', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { appIcons: [{ name: 'fooIcon' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'foo.bar': { appIcons: [{ name: 'fooIcon' }] }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -548,11 +642,15 @@ describe('DynamicRoot', () => { }); it('should render with two appIcons', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { appIcons: [{ name: 'fooIcon' }, { name: 'foo2Icon' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'foo.bar': { appIcons: [{ name: 'fooIcon' }, { name: 'foo2Icon' }] }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -566,11 +664,15 @@ describe('DynamicRoot', () => { }); it('should render with one appIcon from nonexistent plugin', async () => { - process.env = mockProcessEnv({ - 'doesnt.exist': { appIcons: [{ name: 'fooIcon' }] }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + const dynamicPlugins = { + frontend: { + 'doesnt.exist': { appIcons: [{ name: 'fooIcon' }] }, + }, + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -585,24 +687,28 @@ describe('DynamicRoot', () => { it('should bind routes on routeBindings target', async () => { const createAppSpy = jest.spyOn(appDefaults, 'createApp'); - process.env = mockProcessEnv({ - 'foo.bar': { - routeBindings: { - targets: [ - { importName: 'fooPluginTarget' }, - { importName: 'fooPlugin' }, - ], - bindings: [ - { - bindTarget: 'fooPluginTarget.externalRoutes', - bindMap: { barTarget: 'fooPlugin.routes.bar' }, - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + routeBindings: { + targets: [ + { importName: 'fooPluginTarget' }, + { importName: 'fooPlugin' }, + ], + bindings: [ + { + bindTarget: 'fooPluginTarget.externalRoutes', + bindMap: { barTarget: 'fooPlugin.routes.bar' }, + }, + ], + }, }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -623,27 +729,31 @@ describe('DynamicRoot', () => { it('should bind routes on routeBindings target with a custom name', async () => { const createAppSpy = jest.spyOn(appDefaults, 'createApp'); - process.env = mockProcessEnv({ - 'foo.bar': { - routeBindings: { - targets: [ - { - importName: 'fooPluginTarget', - name: 'fooPluginTargetWithCustomName', - }, - { importName: 'fooPlugin' }, - ], - bindings: [ - { - bindTarget: 'fooPluginTargetWithCustomName.externalRoutes', - bindMap: { barTarget: 'fooPlugin.routes.bar' }, - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + routeBindings: { + targets: [ + { + importName: 'fooPluginTarget', + name: 'fooPluginTargetWithCustomName', + }, + { importName: 'fooPlugin' }, + ], + bindings: [ + { + bindTarget: 'fooPluginTargetWithCustomName.externalRoutes', + bindMap: { barTarget: 'fooPlugin.routes.bar' }, + }, + ], + }, }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -663,26 +773,30 @@ describe('DynamicRoot', () => { }); it('should not bind routes on routeBindings target with nonexistent importName', async () => { - process.env = mockProcessEnv({ - 'foo.bar': { - routeBindings: { - targets: [ - { - importName: 'barPlugin', - }, - { importName: 'fooPlugin' }, - ], - bindings: [ - { - bindTarget: 'barPlugin.externalRoutes', - bindMap: { barTarget: 'fooPlugin.routes.bar' }, - }, - ], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + routeBindings: { + targets: [ + { + importName: 'barPlugin', + }, + { importName: 'fooPlugin' }, + ], + bindings: [ + { + bindTarget: 'barPlugin.externalRoutes', + bindMap: { barTarget: 'fooPlugin.routes.bar' }, + }, + ], + }, }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -694,13 +808,17 @@ describe('DynamicRoot', () => { it('should add custom ApiFactory', async () => { const createAppSpy = jest.spyOn(appDefaults, 'createApp'); - process.env = mockProcessEnv({ - 'foo.bar': { - apiFactories: [{ importName: 'fooPluginApi' }], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + apiFactories: [{ importName: 'fooPluginApi' }], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -715,13 +833,17 @@ describe('DynamicRoot', () => { it('should not add custom ApiFactory with nonexistent importName', async () => { const createAppSpy = jest.spyOn(appDefaults, 'createApp'); - process.env = mockProcessEnv({ - 'foo.bar': { - apiFactories: [{ importName: 'barPluginApi' }], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + apiFactories: [{ importName: 'barPluginApi' }], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); @@ -735,13 +857,17 @@ describe('DynamicRoot', () => { it('should not add custom ApiFactory with nonexistent module', async () => { const createAppSpy = jest.spyOn(appDefaults, 'createApp'); - process.env = mockProcessEnv({ - 'foo.bar': { - apiFactories: [{ importName: 'fooPluginApi', module: 'BarPlugin' }], + const dynamicPlugins = { + frontend: { + 'foo.bar': { + apiFactories: [{ importName: 'fooPluginApi', module: 'BarPlugin' }], + }, }, - }); - const appConfig = await loadAppConfig(); - const rendered = await renderWithEffects(); + }; + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); await waitFor(async () => { expect(rendered.baseElement).toBeInTheDocument(); expect(rendered.getByTestId('isLoadingFinished')).toBeInTheDocument(); diff --git a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx index fdf07c91ae..85db9a26ac 100644 --- a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx +++ b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx @@ -15,13 +15,13 @@ import DynamicRootContext, { ScalprumMountPointConfig, } from './DynamicRootContext'; import extractDynamicConfig, { + DynamicPluginConfig, configIfToCallable, } from '../../utils/dynamicUI/extractDynamicConfig'; import initializeRemotePlugins from '../../utils/dynamicUI/initializeRemotePlugins'; import defaultAppComponents from './defaultAppComponents'; import bindAppRoutes from '../../utils/dynamicUI/bindAppRoutes'; import Loader from './Loader'; -import { AppConfig } from '@backstage/config'; export type StaticPlugins = Record< string, @@ -38,16 +38,14 @@ type EntityTabMap = Record; export const DynamicRoot = ({ afterInit, apis: staticApis, - appConfig, - baseFrontendConfig, + dynamicPlugins, staticPluginStore = {}, scalprumConfig, }: { afterInit: () => Promise<{ default: React.ComponentType }>; // Static APIs apis: AnyApiFactory[]; - appConfig: AppConfig[]; - baseFrontendConfig: AppConfig; + dynamicPlugins: DynamicPluginConfig; staticPluginStore?: StaticPlugins; scalprumConfig: AppsConfig; }) => { @@ -71,10 +69,7 @@ export const DynamicRoot = ({ mountPoints, routeBindings, routeBindingTargets, - } = await extractDynamicConfig({ - frontendAppConfig: baseFrontendConfig, - appConfig, - }); + } = await extractDynamicConfig(dynamicPlugins); const requiredModules = [ ...routeBindingTargets.map(({ scope, module }) => ({ @@ -283,7 +278,6 @@ export const DynamicRoot = ({ {} as EntityTabMap, ); if (!app.current) { - const fullConfig = [baseFrontendConfig, ...appConfig]; app.current = createApp({ apis: [...staticApis, ...remoteApis], bindRoutes({ bind }) { @@ -293,7 +287,6 @@ export const DynamicRoot = ({ plugins: Object.values(staticPluginStore).map(entry => entry.plugin), themes, components: defaultAppComponents, - configLoader: async () => Promise.resolve(fullConfig), }); } @@ -310,8 +303,7 @@ export const DynamicRoot = ({ }); }, [ afterInit, - appConfig, - baseFrontendConfig, + dynamicPlugins, pluginStore, scalprumConfig, staticApis, diff --git a/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx b/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx index d4946a031c..d55ddfa085 100644 --- a/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx +++ b/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx @@ -12,6 +12,7 @@ import useAsync from 'react-use/lib/useAsync'; import Loader from './Loader'; import { AppConfig } from '@backstage/config'; import { DynamicRoot, StaticPlugins } from './DynamicRoot'; +import { DynamicPluginConfig } from '../../utils/dynamicUI/extractDynamicConfig'; const ScalprumRoot = ({ apis, @@ -27,19 +28,23 @@ const ScalprumRoot = ({ }) => { const { loading, value } = useAsync( async (): Promise<{ - appConfig: AppConfig[]; + dynamicPlugins: DynamicPluginConfig; baseUrl: string; scalprumConfig?: AppsConfig; }> => { const appConfig = overrideBaseUrlConfigs(await defaultConfigLoader()); - const reader = ConfigReader.fromConfigs(appConfig); + const reader = ConfigReader.fromConfigs([ + ...appConfig, + baseFrontendConfig ? baseFrontendConfig : { context: '', data: {} }, + ]); const baseUrl = reader.getString('backend.baseUrl'); + const dynamicPlugins = reader.get('dynamicPlugins'); try { const scalprumConfig: AppsConfig = await fetch( `${baseUrl}/api/scalprum/plugins`, ).then(r => r.json()); return { - appConfig, + dynamicPlugins, baseUrl, scalprumConfig, }; @@ -49,7 +54,7 @@ const ScalprumRoot = ({ `Failed to fetch scalprum configuration: ${JSON.stringify(err)}`, ); return { - appConfig, + dynamicPlugins, baseUrl, scalprumConfig: {}, }; @@ -59,7 +64,7 @@ const ScalprumRoot = ({ if (loading && !value) { return ; } - const { appConfig, baseUrl, scalprumConfig } = value || {}; + const { dynamicPlugins, baseUrl, scalprumConfig } = value || {}; return ( diff --git a/packages/app/src/components/admin/AdminTabs.test.tsx b/packages/app/src/components/admin/AdminTabs.test.tsx index db83fc58d1..dad1dbf225 100644 --- a/packages/app/src/components/admin/AdminTabs.test.tsx +++ b/packages/app/src/components/admin/AdminTabs.test.tsx @@ -5,13 +5,13 @@ import { removeScalprum } from '@scalprum/core'; import { renderWithEffects } from '@backstage/test-utils'; import AppBase from '../AppBase/AppBase'; import { act } from 'react-dom/test-utils'; -import { AppConfig } from '@backstage/config'; import * as useAsync from 'react-use/lib/useAsync'; import initializeRemotePlugins from '../../utils/dynamicUI/initializeRemotePlugins'; +import { defaultConfigLoader } from '@backstage/core-app-api'; const DynamicRoot = React.lazy(() => import('../DynamicRoot/DynamicRoot')); const mockAppInner = () => ; -const MockApp = ({ appConfig }: { appConfig: AppConfig[] }) => ( +const MockApp = ({ dynamicPlugins }: { dynamicPlugins: any }) => ( ( default: mockAppInner, }) } - appConfig={appConfig} - baseFrontendConfig={{ context: 'frontend', data: {} }} + dynamicPlugins={dynamicPlugins} scalprumConfig={{}} /> @@ -78,30 +77,6 @@ jest.mock('../home/HomePage', () => ({ __esModule: true, })); -// Ensure the correct configuration is picked up by the rendered app -jest.mock('@backstage/config', () => { - const oldModule = jest.requireActual('@backstage/config'); - const OldConfigReader = oldModule.ConfigReader; - const FakeConfigReader = class { - _instance: any = undefined; - constructor(args: any) { - this._instance = new OldConfigReader(args); - } - static fromConfigs(args: any) { - const answer = OldConfigReader.fromConfigs([ - ...[Array.isArray(args) ? args : []], - ...(JSON.parse(process.env.APP_CONFIG || '[]') as Array), - ]); - return answer; - } - }; - return { - ...oldModule, - ConfigReader: FakeConfigReader, - __esModule: true, - }; -}); - const mockInitializeRemotePlugins = jest.fn() as jest.MockedFunction< typeof initializeRemotePlugins >; @@ -110,22 +85,26 @@ jest.mock('../../utils/dynamicUI/initializeRemotePlugins', () => ({ __esModule: true, })); -const createAppConfig = (dynamicPluginsConfig: { [key: string]: any }) => [ - { - data: { - app: { title: 'Test' }, - backend: { baseUrl: 'http://localhost:7007' }, - techdocs: { - storageUrl: 'http://localhost:7007/api/techdocs/static/docs', - }, - auth: { environment: 'development' }, - dynamicPlugins: { - frontend: dynamicPluginsConfig, +const loadTestConfig = async (dynamicPlugins: any) => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { title: 'Test' }, + backend: { baseUrl: 'http://localhost:7007' }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + auth: { environment: 'development' }, + dynamicPlugins, + }, + context: 'test', }, - }, - context: 'test', - }, -]; + ] as any, + }; + await defaultConfigLoader(); +}; const consoleSpy = jest.spyOn(console, 'warn'); @@ -162,15 +141,19 @@ describe('AdminTabs', () => { }); it('Should not be available when not configured', async () => { - const appConfig = createAppConfig({ - 'test-plugin': { - dynamicRoutes: [], - mountPoints: [], + const dynamicPlugins = { + frontend: { + 'test-plugin': { + dynamicRoutes: [], + mountPoints: [], + }, }, - }); - process.env = { NODE_ENV: 'test', APP_CONFIG: JSON.stringify(appConfig) }; + }; initialEntries = ['/']; - const rendered = await renderWithEffects(); + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); expect(rendered.baseElement).toBeInTheDocument(); const home = rendered.queryByText('Home'); const administration = rendered.queryByText('Administration'); @@ -179,15 +162,19 @@ describe('AdminTabs', () => { }); it('Should be available when configured', async () => { - const appConfig = createAppConfig({ - 'test-plugin': { - dynamicRoutes: [{ path: '/admin/plugins' }], - mountPoints: [{ mountPoint: 'admin.page.plugins/cards' }], + const dynamicPlugins = { + frontend: { + 'test-plugin': { + dynamicRoutes: [{ path: '/admin/plugins' }], + mountPoints: [{ mountPoint: 'admin.page.plugins/cards' }], + }, }, - }); + }; initialEntries = ['/']; - process.env = { NODE_ENV: 'test', APP_CONFIG: JSON.stringify(appConfig) }; - const rendered = await renderWithEffects(); + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); expect(rendered.baseElement).toBeInTheDocument(); const home = rendered.queryByText('Home'); const administration = rendered.queryByText('Administration'); @@ -196,15 +183,19 @@ describe('AdminTabs', () => { }); it('Should route to the plugin tab when configured', async () => { - const appConfig = createAppConfig({ - 'test-plugin': { - dynamicRoutes: [{ path: '/admin/plugins' }], - mountPoints: [{ mountPoint: 'admin.page.plugins/cards' }], + const dynamicPlugins = { + frontend: { + 'test-plugin': { + dynamicRoutes: [{ path: '/admin/plugins' }], + mountPoints: [{ mountPoint: 'admin.page.plugins/cards' }], + }, }, - }); + }; initialEntries = ['/']; - process.env = { NODE_ENV: 'test', APP_CONFIG: JSON.stringify(appConfig) }; - const rendered = await renderWithEffects(); + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); expect(rendered.baseElement).toBeInTheDocument(); await act(() => { rendered.getByText('Administration').click(); @@ -214,15 +205,19 @@ describe('AdminTabs', () => { }); it('Should route to the rbac tab when configured', async () => { - const appConfig = createAppConfig({ - 'test-plugin': { - dynamicRoutes: [{ path: '/admin/rbac' }], - mountPoints: [{ mountPoint: 'admin.page.rbac/cards' }], + const dynamicPlugins = { + frontend: { + 'test-plugin': { + dynamicRoutes: [{ path: '/admin/rbac' }], + mountPoints: [{ mountPoint: 'admin.page.rbac/cards' }], + }, }, - }); + }; initialEntries = ['/']; - process.env = { NODE_ENV: 'test', APP_CONFIG: JSON.stringify(appConfig) }; - const rendered = await renderWithEffects(); + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); expect(rendered.baseElement).toBeInTheDocument(); await act(() => { rendered.getByText('Administration').click(); @@ -232,15 +227,19 @@ describe('AdminTabs', () => { }); it("Should fail back to the default tab if the currently routed tab doesn't match the configuration", async () => { - const appConfig = createAppConfig({ - 'test-plugin': { - dynamicRoutes: [{ path: '/admin/rbac' }], - mountPoints: [{ mountPoint: 'admin.page.rbac/cards' }], + const dynamicPlugins = { + frontend: { + 'test-plugin': { + dynamicRoutes: [{ path: '/admin/rbac' }], + mountPoints: [{ mountPoint: 'admin.page.rbac/cards' }], + }, }, - }); + }; initialEntries = ['/admin/plugins']; - process.env = { NODE_ENV: 'test', APP_CONFIG: JSON.stringify(appConfig) }; - const rendered = await renderWithEffects(); + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); // When debugging this test it can be handy to see the entire rendered output // process.stdout.write(`${prettyDOM(rendered.baseElement, 900000)}`); expect(rendered.baseElement).toBeInTheDocument(); @@ -248,15 +247,19 @@ describe('AdminTabs', () => { }); it('Should fail with an error page if routed to but no configuration is defined', async () => { - const appConfig = createAppConfig({ - 'test-plugin': { - dynamicRoutes: [], - mountPoints: [], + const dynamicPlugins = { + frontend: { + 'test-plugin': { + dynamicRoutes: [], + mountPoints: [], + }, }, - }); + }; initialEntries = ['/admin/plugins']; - process.env = { NODE_ENV: 'test', APP_CONFIG: JSON.stringify(appConfig) }; - const rendered = await renderWithEffects(); + await loadTestConfig(dynamicPlugins); + const rendered = await renderWithEffects( + , + ); // When debugging this test it can be handy to see the entire rendered output // process.stdout.write(`${prettyDOM(rendered.baseElement, 900000)}`); expect(rendered.baseElement).toBeInTheDocument(); diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts index 02d86a9e89..35387d7475 100644 --- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts +++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts @@ -1,5 +1,5 @@ -import { AppConfig } from '@backstage/config'; import extractDynamicConfig, { + DynamicPluginConfig, conditionsArrayMapper, configIfToCallable, } from './extractDynamicConfig'; @@ -143,10 +143,8 @@ describe('extractDynamicConfig', () => { 'no frontend dynamic plugins are defined', { dynamicPlugins: { frontend: {} } }, ], - ])('returns empty data when %s', async (_, source) => { - const config = await extractDynamicConfig({ - appConfig: [source] as AppConfig[], - }); + ])('returns empty data when %s', (_, source) => { + const config = extractDynamicConfig(source as DynamicPluginConfig); expect(config).toEqual({ routeBindings: [], dynamicRoutes: [], @@ -430,16 +428,9 @@ describe('extractDynamicConfig', () => { ], }, ], - ])('parses %s', async (_, source, output) => { - const config = await extractDynamicConfig({ - appConfig: [ - { - context: 'foo', - data: { - dynamicPlugins: { frontend: { 'janus-idp.plugin-foo': source } }, - }, - }, - ] as AppConfig[], + ])('parses %s', (_, source: any, output) => { + const config = extractDynamicConfig({ + frontend: { 'janus-idp.plugin-foo': source }, }); expect(config).toEqual({ routeBindings: [], diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts index f517c647a1..9dc064596d 100644 --- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts +++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts @@ -77,20 +77,12 @@ type CustomProperties = { apiFactories?: ApiFactory[]; }; -type AppConfig = { - context: string; - data: { - dynamicPlugins?: { - frontend?: { - [key: string]: CustomProperties; - }; - }; - }; +type FrontendConfig = { + [key: string]: CustomProperties; }; -type ExtractDynamicConfigProps = { - appConfig?: AppConfig[]; - frontendAppConfig?: AppConfig; +export type DynamicPluginConfig = { + frontend?: FrontendConfig; }; type DynamicConfig = { @@ -104,44 +96,14 @@ type DynamicConfig = { }; /** - * Converts all available configuration sources into the data structures - * needed by the DynamicRoot component to wire the app together. Accepts - * an initial configuration for any statically linked frontend plugins that - * need to display UI elements on dynamic frontend pages. - * - * @param frontendAppConfig - */ -async function extractDynamicConfig({ - appConfig = [], - frontendAppConfig = { context: '', data: {} }, -}: ExtractDynamicConfigProps) { - const initialDynamicConfig = appConfigsToDynamicConfig([frontendAppConfig]); - const dynamicConfig = appConfigsToDynamicConfig(appConfig, { - apiFactories: initialDynamicConfig.apiFactories, - appIcons: initialDynamicConfig.appIcons, - dynamicRoutes: initialDynamicConfig.dynamicRoutes.filter(dynamicRoute => - doesConfigContain(dynamicRoute, 'dynamicRoutes', appConfig), - ), - entityTabs: initialDynamicConfig.entityTabs, - mountPoints: initialDynamicConfig.mountPoints.filter(mountPoint => - doesConfigContain(mountPoint, 'mountPoints', appConfig), - ), - routeBindings: initialDynamicConfig.routeBindings, - routeBindingTargets: initialDynamicConfig.routeBindingTargets, - }); - return dynamicConfig; -} - -/** - * Converts an array of AppConfig objects to a dynamic frontend - * configuration structure - * @param appConfigs - * @param initialDynamicConfig - * @returns + * Converts the dynamic plugin configuration structure to the data structure + * required by the dynamic UI, substituting in any defaults as needed */ -function appConfigsToDynamicConfig( - appConfigs: AppConfig[], - initialDynamicConfig: DynamicConfig = { +function extractDynamicConfig( + dynamicPlugins: DynamicPluginConfig = { frontend: {} }, +) { + const frontend = dynamicPlugins.frontend || {}; + const config: DynamicConfig = { apiFactories: [], appIcons: [], dynamicRoutes: [], @@ -149,146 +111,96 @@ function appConfigsToDynamicConfig( mountPoints: [], routeBindings: [], routeBindingTargets: [], - }, -) { - return appConfigs.reduce((acc, { data }) => { - if (data?.dynamicPlugins?.frontend) { - acc.dynamicRoutes.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce( - (pluginSet, [scope, customProperties]) => { - pluginSet.push( - ...(customProperties.dynamicRoutes ?? []).map(route => ({ - ...route, - module: route.module ?? 'PluginRoot', - importName: route.importName ?? 'default', - scope, - })), - ); - return pluginSet; - }, - [], - ), + }; + config.dynamicRoutes = Object.entries(frontend).reduce( + (pluginSet, [scope, customProperties]) => { + pluginSet.push( + ...(customProperties.dynamicRoutes ?? []).map(route => ({ + ...route, + module: route.module ?? 'PluginRoot', + importName: route.importName ?? 'default', + scope, + })), ); - acc.routeBindings.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce( - (pluginSet, [_, customProperties]) => { - pluginSet.push(...(customProperties.routeBindings?.bindings ?? [])); - return pluginSet; - }, - [], - ), + return pluginSet; + }, + [], + ); + config.routeBindings = Object.entries(frontend).reduce( + (pluginSet, [_, customProperties]) => { + pluginSet.push(...(customProperties.routeBindings?.bindings ?? [])); + return pluginSet; + }, + [], + ); + config.routeBindingTargets = Object.entries(frontend).reduce( + (pluginSet, [scope, customProperties]) => { + pluginSet.push( + ...(customProperties.routeBindings?.targets ?? []).map(target => ({ + ...target, + module: target.module ?? 'PluginRoot', + name: target.name ?? target.importName, + scope, + })), ); - acc.routeBindingTargets.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce( - (pluginSet, [scope, customProperties]) => { - pluginSet.push( - ...(customProperties.routeBindings?.targets ?? []).map( - target => ({ - ...target, - module: target.module ?? 'PluginRoot', - name: target.name ?? target.importName, - scope, - }), - ), - ); - return pluginSet; - }, - [], - ), + return pluginSet; + }, + [], + ); + config.mountPoints = Object.entries(frontend).reduce( + (accMountPoints, [scope, { mountPoints }]) => { + accMountPoints.push( + ...(mountPoints ?? []).map(mountPoint => ({ + ...mountPoint, + module: mountPoint.module ?? 'PluginRoot', + importName: mountPoint.importName ?? 'default', + scope, + })), ); - - acc.mountPoints.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce( - (accMountPoints, [scope, { mountPoints }]) => { - accMountPoints.push( - ...(mountPoints ?? []).map(point => ({ - ...point, - module: point.module ?? 'PluginRoot', - importName: point.importName ?? 'default', - scope, - })), - ); - return accMountPoints; - }, - [], - ), + return accMountPoints; + }, + [], + ); + config.appIcons = Object.entries(frontend).reduce( + (accAppIcons, [scope, { appIcons }]) => { + accAppIcons.push( + ...(appIcons ?? []).map(icon => ({ + ...icon, + module: icon.module ?? 'PluginRoot', + importName: icon.importName ?? 'default', + scope, + })), ); - - acc.appIcons.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce( - (accAppIcons, [scope, { appIcons }]) => { - accAppIcons.push( - ...(appIcons ?? []).map(icon => ({ - ...icon, - module: icon.module ?? 'PluginRoot', - importName: icon.importName ?? 'default', - scope, - })), - ); - return accAppIcons; - }, - [], - ), + return accAppIcons; + }, + [], + ); + config.apiFactories = Object.entries(frontend).reduce( + (accApiFactories, [scope, { apiFactories }]) => { + accApiFactories.push( + ...(apiFactories ?? []).map(api => ({ + module: api.module ?? 'PluginRoot', + importName: api.importName ?? 'default', + scope, + })), ); - - acc.apiFactories.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce( - (accApiFactories, [scope, { apiFactories }]) => { - accApiFactories.push( - ...(apiFactories ?? []).map(api => ({ - module: api.module ?? 'PluginRoot', - importName: api.importName ?? 'default', - scope, - })), - ); - return accApiFactories; - }, - [], - ), - ); - - acc.entityTabs.push( - ...Object.entries(data.dynamicPlugins.frontend).reduce< - EntityTabEntry[] - >((accEntityTabs, [scope, { entityTabs }]) => { - accEntityTabs.push( - ...(entityTabs ?? []).map(entityTab => ({ - ...entityTab, - scope, - })), - ); - return accEntityTabs; - }, []), + return accApiFactories; + }, + [], + ); + config.entityTabs = Object.entries(frontend).reduce( + (accEntityTabs, [scope, { entityTabs }]) => { + accEntityTabs.push( + ...(entityTabs ?? []).map(entityTab => ({ + ...entityTab, + scope, + })), ); - } - return acc; - }, initialDynamicConfig); -} - -/** - * Check the app config to see if the given entry has been configured in an - * app-config.yaml file. Used to override static plugin configuration. - * - * @param entry - * @param attribute - * @param appConfigs - * @returns - */ -function doesConfigContain( - entry: DynamicRoute | MountPoint, - attribute: string, - appConfigs: AppConfig[], -) { - return appConfigs - .map( - (appConfig: AppConfig) => appConfig.data.dynamicPlugins?.frontend || {}, - ) - .filter(pluginConfig => { - return Object.keys(pluginConfig).find(scope => scope === entry.scope); - }) - .reduce((_acc, curr: any) => { - return curr[entry.scope][attribute] === undefined; - }, true); + return accEntityTabs; + }, + [], + ); + return config; } /**