core-compat-api: move entity page conversion into convertLegacyApp

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-03-08 11:21:00 +01:00
parent 337f8a6ca2
commit 2f93adeb98
4 changed files with 110 additions and 42 deletions
@@ -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<LegacyBackstagePlugin>(
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;
}
@@ -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<FrontendPlugin | FrontendModule>();
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<ExtensionDefinition>();
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;
}
@@ -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,
-1
View File
@@ -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 {