core-compat-api: recursive collectLecacyRoutes
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-compat-api': minor
|
||||
---
|
||||
|
||||
Discover plugins and routes recursively beneath the root routes in `collectLecacyRoutes`
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<FlatRoutes>
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case
|
||||
if={isKind('component')}
|
||||
children={
|
||||
<EntityLayout>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</EntityLayout>
|
||||
}
|
||||
/>
|
||||
<EntitySwitch.Case>
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Fragment>
|
||||
<Routes>
|
||||
<Route path="/subthing">
|
||||
<ScoreBoardPage />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Fragment>
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
</Route>
|
||||
</FlatRoutes>,
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<unknown>[];
|
||||
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<LegacyBackstagePlugin>(node, 'core.plugin');
|
||||
const routeRef = getComponentData<RouteRef<AnyRouteRefParams>>(
|
||||
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<unknown>[]
|
||||
>();
|
||||
|
||||
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 <RequirePermission>, 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<LegacyBackstagePlugin>(
|
||||
routeElement,
|
||||
'core.plugin',
|
||||
);
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const routeRef = getComponentData<RouteRef>(
|
||||
routeElement,
|
||||
'core.mountPoint',
|
||||
);
|
||||
if (!plugin || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detectedExtensions =
|
||||
createdPluginIds.get(plugin) ??
|
||||
new Array<ExtensionDefinition<unknown>>();
|
||||
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 ? (
|
||||
<Routes>
|
||||
@@ -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: [
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user