From cf5cc4c8a0bdf30fc1c777c348eef0cc514b4563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 29 Nov 2023 16:55:12 +0100 Subject: [PATCH] core-compat-api: recursive collectLecacyRoutes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/nasty-rockets-bathe.md | 5 + packages/core-compat-api/package.json | 25 +-- .../src/collectLegacyRoutes.test.tsx | 144 ++++++++++++++- .../src/collectLegacyRoutes.tsx | 173 ++++++++++++++---- .../src/convertLegacyApp.test.tsx | 2 +- .../src/convertLegacyRouteRef.ts | 15 +- yarn.lock | 1 + 7 files changed, 310 insertions(+), 55 deletions(-) create mode 100644 .changeset/nasty-rockets-bathe.md diff --git a/.changeset/nasty-rockets-bathe.md b/.changeset/nasty-rockets-bathe.md new file mode 100644 index 0000000000..1320b000b7 --- /dev/null +++ b/.changeset/nasty-rockets-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': minor +--- + +Discover plugins and routes recursively beneath the root routes in `collectLecacyRoutes` diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index f713b49cf6..dd602f5380 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -22,9 +22,21 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, + "dependencies": { + "@backstage/core-app-api": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/version-bridge": "workspace:^", + "@types/react": "^16.13.1 || ^17.0.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-puppetdb": "workspace:^", "@backstage/plugin-stackstorm": "workspace:^", "@oriflame/backstage-plugin-score-card": "^0.7.0", @@ -33,16 +45,5 @@ }, "files": [ "dist" - ], - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-router-dom": "6.0.0-beta.0 || ^6.3.0" - }, - "dependencies": { - "@backstage/core-app-api": "workspace:^", - "@backstage/core-plugin-api": "workspace:^", - "@backstage/frontend-plugin-api": "workspace:^", - "@backstage/version-bridge": "workspace:^", - "@types/react": "^16.13.1 || ^17.0.0" - } + ] } diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index 3dd9f330cc..862ef68bb5 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -15,11 +15,19 @@ */ import { FlatRoutes } from '@backstage/core-app-api'; +import { + CatalogEntityPage, + CatalogIndexPage, + EntityAboutCard, + EntityLayout, + EntitySwitch, + isKind, +} from '@backstage/plugin-catalog'; import { PuppetDbPage } from '@backstage/plugin-puppetdb'; import { StackstormPage } from '@backstage/plugin-stackstorm'; import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; -import React from 'react'; -import { Route } from 'react-router-dom'; +import React, { Fragment } from 'react'; +import { Route, Routes } from 'react-router-dom'; import { collectLegacyRoutes } from './collectLegacyRoutes'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -89,7 +97,7 @@ describe('collectLegacyRoutes', () => { defaultConfig: { path: 'puppetdb' }, }, { - id: 'page:puppetDb/2', + id: 'page:puppetDb/1', attachTo: { id: 'core/routes', input: 'routes' }, disabled: false, defaultConfig: { path: 'puppetdb' }, @@ -103,4 +111,134 @@ describe('collectLegacyRoutes', () => { }, ]); }); + + it('supports recursion into children, including passing through fragments', () => { + const collected = collectLegacyRoutes( + + } /> + } + > + + + + + } + /> + + + + + + + + + + + + + + + + , + ); + + expect( + collected.map(p => ({ + id: p.id, + extensions: toInternalBackstagePlugin(p).extensions.map(e => ({ + id: e.id, + attachTo: e.attachTo, + disabled: e.disabled, + defaultConfig: e.configSchema?.parse({}), + })), + })), + ).toEqual([ + { + id: 'catalog', + extensions: [ + { + id: 'page:catalog', + attachTo: { id: 'core/routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'catalog' }, + }, + { + id: 'page:catalog/1', + attachTo: { id: 'core/routes', input: 'routes' }, + defaultConfig: { path: 'catalog/:namespace/:kind/:name' }, + disabled: false, + }, + { + id: 'routing-shim:catalog/2', + attachTo: { + id: 'page:catalog/1', + input: 'childRoutingShims', + }, + defaultConfig: undefined, + disabled: false, + }, + { + id: 'routing-shim:catalog/3', + attachTo: { + id: 'routing-shim:catalog/2', + input: 'childRoutingShims', + }, + defaultConfig: undefined, + disabled: false, + }, + { + id: 'routing-shim:catalog/4', + attachTo: { + id: 'routing-shim:catalog/3', + input: 'childRoutingShims', + }, + defaultConfig: undefined, + disabled: false, + }, + { + id: 'api:plugin.catalog.service', + attachTo: { + id: 'core', + input: 'apis', + }, + defaultConfig: undefined, + disabled: false, + }, + { + id: 'api:catalog-react.starred-entities', + attachTo: { + id: 'core', + input: 'apis', + }, + defaultConfig: undefined, + disabled: false, + }, + { + id: 'api:plugin.catalog.entity-presentation', + attachTo: { + id: 'core', + input: 'apis', + }, + defaultConfig: undefined, + disabled: false, + }, + ], + }, + { + id: 'score-card', + extensions: [ + { + id: 'api:plugin.scoringdata.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + ]); + }); }); diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index 92bced29a2..729ead2786 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -14,20 +14,24 @@ * limitations under the License. */ -import React, { ReactNode } from 'react'; -import { - createApiExtension, - createPageExtension, - createPlugin, - BackstagePlugin, - ExtensionDefinition, -} from '@backstage/frontend-plugin-api'; -import { Route, Routes } from 'react-router-dom'; import { + AnyRouteRefParams, BackstagePlugin as LegacyBackstagePlugin, RouteRef, getComponentData, } from '@backstage/core-plugin-api'; +import { + BackstagePlugin, + ExtensionDefinition, + coreExtensionData, + createApiExtension, + createExtension, + createExtensionInput, + createPageExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; +import React, { Children, ReactNode, isValidElement } from 'react'; +import { Route, Routes } from 'react-router-dom'; import { convertLegacyRouteRef } from './convertLegacyRouteRef'; /* @@ -58,58 +62,150 @@ Existing tasks: */ +// Creates a shim extension whose purpose is to build up the tree (anchored at +// the root page) of paths/routeRefs so that the app can bind them properly. +function makeRoutingShimExtension(options: { + name: string; + parentExtensionId: string; + routePath?: string; + routeRef?: RouteRef; +}) { + const { name, parentExtensionId, routePath, routeRef } = options; + return createExtension({ + kind: 'routing-shim', + name, + attachTo: { id: parentExtensionId, input: 'childRoutingShims' }, + inputs: { + childRoutingShims: createExtensionInput({ + routePath: coreExtensionData.routePath.optional(), + routeRef: coreExtensionData.routeRef.optional(), + }), + }, + output: { + routePath: coreExtensionData.routePath.optional(), + routeRef: coreExtensionData.routeRef.optional(), + }, + factory: () => ({ + routePath, + routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, + }), + }); +} + +function visitRouteChildren(options: { + children: ReactNode; + parentExtensionId: string; + context: { + pluginId: string; + extensions: ExtensionDefinition[]; + getUniqueName: () => string; + discoverPlugin: (plugin: LegacyBackstagePlugin) => void; + }; +}): void { + const { children, parentExtensionId, context } = options; + const { pluginId, extensions, getUniqueName, discoverPlugin } = context; + + Children.forEach(children, node => { + if (!isValidElement(node)) { + return; + } + + const plugin = getComponentData(node, 'core.plugin'); + const routeRef = getComponentData>( + node, + 'core.mountPoint', + ); + const routePath: string | undefined = node.props?.path; + + if (plugin) { + // We just mark the plugin as discovered, but don't change the context + discoverPlugin(plugin); + } + + let nextParentExtensionId = parentExtensionId; + if (routeRef || routePath) { + const nextParentExtensionName = getUniqueName(); + nextParentExtensionId = `routing-shim:${pluginId}/${nextParentExtensionName}`; + extensions.push( + makeRoutingShimExtension({ + name: nextParentExtensionName, + parentExtensionId, + routePath, + routeRef, + }), + ); + } + + visitRouteChildren({ + children: node.props.children, + parentExtensionId: nextParentExtensionId, + context, + }); + }); +} + /** @public */ export function collectLegacyRoutes( flatRoutesElement: JSX.Element, ): BackstagePlugin[] { - const createdPluginIds = new Map< + const pluginExtensions = new Map< LegacyBackstagePlugin, ExtensionDefinition[] >(); + const getUniqueName = (() => { + let currentIndex = 1; + return () => String(currentIndex++); + })(); + + const getPluginExtensions = (plugin: LegacyBackstagePlugin) => { + let extensions = pluginExtensions.get(plugin); + if (!extensions) { + extensions = []; + pluginExtensions.set(plugin, extensions); + } + return extensions; + }; + React.Children.forEach( flatRoutesElement.props.children, (route: ReactNode) => { - if (!React.isValidElement(route)) { - return; - } - // TODO(freben): Handle feature flag and permissions framework wrapper elements - if (route.type !== Route) { + if (!React.isValidElement(route) || route.type !== Route) { return; } const routeElement = route.props.element; - - // TODO: to support deeper extension component, e.g. hidden within , use https://github.com/backstage/backstage/blob/518a34646b79ec2028cc0ed6bc67d4366c51c4d6/packages/core-app-api/src/routing/collectors.tsx#L69 + const path: string | undefined = route.props.path; const plugin = getComponentData( routeElement, 'core.plugin', ); - if (!plugin) { - return; - } - const routeRef = getComponentData( routeElement, 'core.mountPoint', ); + if (!plugin || !path) { + return; + } - const detectedExtensions = - createdPluginIds.get(plugin) ?? - new Array>(); - createdPluginIds.set(plugin, detectedExtensions); + const extensions = getPluginExtensions(plugin); + const pageExtensionName = extensions.length ? getUniqueName() : undefined; + const pageExtensionId = `page:${plugin.getId()}${ + pageExtensionName ? `/${pageExtensionName}` : pageExtensionName + }`; - const path: string = route.props.path; - - detectedExtensions.push( + extensions.push( createPageExtension({ - name: detectedExtensions.length - ? String(detectedExtensions.length + 1) - : undefined, + name: pageExtensionName, defaultPath: path[0] === '/' ? path.slice(1) : path, routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, - + inputs: { + childRoutingShims: createExtensionInput({ + routePath: coreExtensionData.routePath.optional(), + routeRef: coreExtensionData.routeRef.optional(), + }), + }, loader: async () => route.props.children ? ( @@ -122,10 +218,21 @@ export function collectLegacyRoutes( ), }), ); + + visitRouteChildren({ + children: route.props.children, + parentExtensionId: pageExtensionId, + context: { + pluginId: plugin.getId(), + extensions, + getUniqueName, + discoverPlugin: getPluginExtensions, + }, + }); }, ); - return Array.from(createdPluginIds).map(([plugin, extensions]) => + return Array.from(pluginExtensions).map(([plugin, extensions]) => createPlugin({ id: plugin.getId(), extensions: [ diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx index 4a5be57bc8..9fae0128b0 100644 --- a/packages/core-compat-api/src/convertLegacyApp.test.tsx +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -97,7 +97,7 @@ describe('convertLegacyApp', () => { defaultConfig: { path: 'puppetdb' }, }, { - id: 'page:puppetDb/2', + id: 'page:puppetDb/1', attachTo: { id: 'core/routes', input: 'routes' }, disabled: false, defaultConfig: { path: 'puppetdb' }, diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index 2495f3719c..c5ee603722 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -91,6 +91,7 @@ export function convertLegacyRouteRef( if (type === 'absolute') { const legacyRef = ref as LegacyRouteRef; + const legacyRefStr = String(legacyRef); const newRef = toInternalRouteRef( createRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], @@ -104,18 +105,19 @@ export function convertLegacyRouteRef( return newRef.getParams(); }, getDescription() { - return newRef.getDescription(); + return legacyRefStr; }, setId(id: string) { newRef.setId(id); }, toString() { - return newRef.toString(); + return legacyRefStr; }, }); } if (type === 'sub') { const legacyRef = ref as LegacySubRouteRef; + const legacyRefStr = String(legacyRef); const newRef = toInternalSubRouteRef( createSubRouteRef({ path: legacyRef.path, @@ -133,15 +135,16 @@ export function convertLegacyRouteRef( return newRef.getParent(); }, getDescription() { - return newRef.getDescription(); + return legacyRefStr; }, toString() { - return newRef.toString(); + return legacyRefStr; }, }); } if (type === 'external') { const legacyRef = ref as LegacyExternalRouteRef; + const legacyRefStr = String(legacyRef); const newRef = toInternalExternalRouteRef( createExternalRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], @@ -157,13 +160,13 @@ export function convertLegacyRouteRef( return newRef.getParams(); }, getDescription() { - return newRef.getDescription(); + return legacyRefStr; }, setId(id: string) { newRef.setId(id); }, toString() { - return newRef.toString(); + return legacyRefStr; }, }); } diff --git a/yarn.lock b/yarn.lock index f38145a560..1f22be1c74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3856,6 +3856,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-puppetdb": "workspace:^" "@backstage/plugin-stackstorm": "workspace:^" "@backstage/version-bridge": "workspace:^"