From 2f93adeb98771a6de6e792c1323e968aa270748c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 8 Mar 2025 11:21:00 +0100 Subject: [PATCH] core-compat-api: move entity page conversion into convertLegacyApp Signed-off-by: Patrik Oldsberg --- ...tyPage.ts => collectEntityPageContents.ts} | 46 ++++++----- .../src/collectLegacyRoutes.tsx | 81 ++++++++++++++----- .../core-compat-api/src/convertLegacyApp.ts | 24 +++++- packages/core-compat-api/src/index.ts | 1 - 4 files changed, 110 insertions(+), 42 deletions(-) rename packages/core-compat-api/src/{convertLegacyEntityPage.ts => collectEntityPageContents.ts} (88%) diff --git a/packages/core-compat-api/src/convertLegacyEntityPage.ts b/packages/core-compat-api/src/collectEntityPageContents.ts similarity index 88% rename from packages/core-compat-api/src/convertLegacyEntityPage.ts rename to packages/core-compat-api/src/collectEntityPageContents.ts index 7df99240f1..c2a3ba2145 100644 --- a/packages/core-compat-api/src/convertLegacyEntityPage.ts +++ b/packages/core-compat-api/src/collectEntityPageContents.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import { ApiHolder, getComponentData } from '@backstage/core-plugin-api'; import { - createFrontendPlugin, - ExtensionDefinition, - FrontendModule, - FrontendPlugin, -} from '@backstage/frontend-plugin-api'; + ApiHolder, + getComponentData, + BackstagePlugin as LegacyBackstagePlugin, +} from '@backstage/core-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import React from 'react'; import { EntityCardBlueprint, @@ -72,14 +71,18 @@ function invertFilter(ifFunc?: EntityFilter): EntityFilter { return (entity, ctx) => !ifFunc(entity, ctx); } -export function convertLegacyEntityPage( +export function collectEntityPageContents( entityPageElement: React.JSX.Element, -): (FrontendModule | FrontendPlugin)[] { + context: { + discoveryExtension( + extension: ExtensionDefinition, + plugin?: LegacyBackstagePlugin, + ): void; + }, +) { let cardCounter = 1; let routeCounter = 1; - const collected: ExtensionDefinition[] = []; - function traverse(element: React.ReactNode, parentFilter?: EntityFilter) { if (!React.isValidElement(element)) { return; @@ -91,9 +94,9 @@ export function convertLegacyEntityPage( const mergedIf = allFilters(parentFilter, pageNode.if); if (pageNode.path === '/') { - collected.push( + context.discoveryExtension( EntityCardBlueprint.makeWithOverrides({ - name: `discovered-entity-cards-${cardCounter++}`, + name: `discovered-${cardCounter++}`, factory(originalFactory, { apis }) { return originalFactory({ type: 'full', @@ -104,9 +107,11 @@ export function convertLegacyEntityPage( }), ); } else { - collected.push( + const name = `discovered-${routeCounter++}`; + + context.discoveryExtension( EntityContentBlueprint.makeWithOverrides({ - name: `discovered-entity-route-${routeCounter++}`, + name, factory(originalFactory, { apis }) { return originalFactory({ defaultPath: pageNode.path, @@ -116,6 +121,10 @@ export function convertLegacyEntityPage( }); }, }), + getComponentData( + pageNode.children, + 'core.plugin', + ), ); } } @@ -151,13 +160,6 @@ export function convertLegacyEntityPage( } traverse(entityPageElement); - - return [ - createFrontendPlugin({ - id: 'converted-orphan-entity-content', - extensions: collected, - }), - ]; } type EntityRoute = { @@ -192,7 +194,7 @@ function wrapAsyncEntityFilter( if (!loggedError) { // eslint-disable-next-line no-console console.error( - `convertLegacyEntityPage does not support async entity filters, skipping filter ${asyncFilter}`, + `collectEntityPageContents does not support async entity filters, skipping filter ${asyncFilter}`, ); loggedError = true; } diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index b0a55f9ddd..3cf16b96c0 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -30,6 +30,8 @@ import { createFrontendPlugin, ApiBlueprint, PageBlueprint, + FrontendModule, + createFrontendModule, } from '@backstage/frontend-plugin-api'; import React, { Children, ReactNode, isValidElement } from 'react'; import { Route, Routes } from 'react-router-dom'; @@ -38,6 +40,7 @@ import { convertLegacyRouteRefs, } from './convertLegacyRouteRef'; import { compatWrapper } from './compatWrapper'; +import { collectEntityPageContents } from './collectEntityPageContents'; /* @@ -102,7 +105,7 @@ function makeRoutingShimExtension(options: { }); } -function visitRouteChildren(options: { +export function visitRouteChildren(options: { children: ReactNode; parentExtensionId: string; context: { @@ -157,7 +160,10 @@ function visitRouteChildren(options: { /** @internal */ export function collectLegacyRoutes( flatRoutesElement: JSX.Element, -): FrontendPlugin[] { + entityPage?: JSX.Element, +): (FrontendPlugin | FrontendModule)[] { + const output = new Array(); + const pluginExtensions = new Map< LegacyBackstagePlugin, ExtensionDefinition[] @@ -262,20 +268,59 @@ export function collectLegacyRoutes( }, ); - return Array.from(pluginExtensions).map(([plugin, extensions]) => - createFrontendPlugin({ - id: plugin.getId(), - extensions: [ - ...extensions, - ...Array.from(plugin.getApis()).map(factory => - ApiBlueprint.make({ - name: factory.api.id, - params: { factory }, - }), - ), - ], - routes: convertLegacyRouteRefs(plugin.routes ?? {}), - externalRoutes: convertLegacyRouteRefs(plugin.externalRoutes ?? {}), - }), - ); + if (entityPage) { + collectEntityPageContents(entityPage, { + discoveryExtension(extension, plugin) { + if (!plugin || plugin.getId() === 'catalog') { + getPluginExtensions(orphanRoutesPlugin).push(extension); + } else { + getPluginExtensions(plugin).push(extension); + } + }, + }); + + const extensions = new Array(); + visitRouteChildren({ + children: entityPage, + parentExtensionId: `page:catalog/entity`, + context: { + pluginId: 'catalog', + extensions, + getUniqueName, + discoverPlugin(plugin) { + if (plugin.getId() !== 'catalog') { + getPluginExtensions(plugin); + } + }, + }, + }); + + output.push( + createFrontendModule({ + pluginId: 'catalog', + extensions, + }), + ); + } + + for (const [plugin, extensions] of pluginExtensions) { + output.push( + createFrontendPlugin({ + id: plugin.getId(), + extensions: [ + ...extensions, + ...Array.from(plugin.getApis()).map(factory => + ApiBlueprint.make({ + name: factory.api.id, + params: { factory }, + }), + ), + ], + routes: convertLegacyRouteRefs(plugin.routes ?? {}), + externalRoutes: convertLegacyRouteRefs(plugin.externalRoutes ?? {}), + }), + ); + } + + return output; } diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 52efc96ee2..eb72cda204 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -59,9 +59,31 @@ function selectChildren( }); } +export interface ConvertLegacyAppOptions { + /** + * By providing an entity page element here it will be split up and converted + * into individual extensions for the catalog plugin in the new frontend + * system. + * + * In order to use this option the entity page and other catalog extensions + * must be removed from the legacy app, and the catalog plugin for the new + * frontend system must be installed instead. + * + * In order for this conversion to work the entity page must be a plain React + * element tree without any component wrapping anywhere between the root + * element and the `EntityLayout.Route` elements. + * + * When enabling this conversion you are likely to encounter duplicate entity + * page content provided both via the old structure and the new plugins. Any + * duplicate content needs to be removed from the old structure. + */ + entityPage?: React.JSX.Element; +} + /** @public */ export function convertLegacyApp( rootElement: React.JSX.Element, + options: ConvertLegacyAppOptions = {}, ): (FrontendPlugin | FrontendModule)[] { if (getComponentData(rootElement, 'core.type') === 'FlatRoutes') { return collectLegacyRoutes(rootElement); @@ -136,7 +158,7 @@ export function convertLegacyApp( disabled: true, }); - const collectedRoutes = collectLegacyRoutes(routesEl); + const collectedRoutes = collectLegacyRoutes(routesEl, options?.entityPage); return [ ...collectedRoutes, diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 5e64dfab21..6c2df56d92 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -19,7 +19,6 @@ export * from './apis'; export { convertLegacyApp } from './convertLegacyApp'; export { convertLegacyAppOptions } from './convertLegacyAppOptions'; -export { convertLegacyEntityPage } from './convertLegacyEntityPage'; export { convertLegacyPlugin } from './convertLegacyPlugin'; export { convertLegacyPageExtension } from './convertLegacyPageExtension'; export {