frontend-app-api: add support for collecting route objects
Co-authored-by: Camila Belo <camilaibs@gmail.com> Co-authored-by: Philipp Hugenroth <philipph@spotify.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { RouteRef, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
createRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { extractRouteInfoFromInstanceTree } from './extractRouteInfoFromInstanceTree';
|
||||
import {
|
||||
coreExtensionData,
|
||||
@@ -23,7 +27,7 @@ import {
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createInstances } from '../wiring/createApp';
|
||||
import { createInstances, toLegacyPlugin } from '../wiring/createApp';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
|
||||
const ref1 = createRouteRef({ id: 'page1' });
|
||||
@@ -39,31 +43,31 @@ function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
|
||||
);
|
||||
}
|
||||
|
||||
// async function routeObj(
|
||||
// path: string,
|
||||
// refs: RouteRef[],
|
||||
// children: any[] = [],
|
||||
// type: 'mounted' | 'gathered' = 'mounted',
|
||||
// backstagePlugin?: BackstagePlugin,
|
||||
// ) {
|
||||
// return {
|
||||
// path: path,
|
||||
// caseSensitive: false,
|
||||
// element: type,
|
||||
// routeRefs: new Set(refs),
|
||||
// children: [
|
||||
// {
|
||||
// path: '*',
|
||||
// caseSensitive: false,
|
||||
// element: 'match-all',
|
||||
// routeRefs: new Set(),
|
||||
// plugins: new Set(),
|
||||
// },
|
||||
// ...children,
|
||||
// ],
|
||||
// plugins: backstagePlugin ? new Set([backstagePlugin]) : new Set(),
|
||||
// };
|
||||
// }
|
||||
function routeObj(
|
||||
path: string,
|
||||
refs: RouteRef[],
|
||||
children: any[] = [],
|
||||
type: 'mounted' | 'gathered' = 'mounted',
|
||||
backstagePlugin?: BackstagePlugin,
|
||||
) {
|
||||
return {
|
||||
path: path,
|
||||
caseSensitive: false,
|
||||
element: type,
|
||||
routeRefs: new Set(refs),
|
||||
children: [
|
||||
{
|
||||
path: '*',
|
||||
caseSensitive: false,
|
||||
element: 'match-all',
|
||||
routeRefs: new Set(),
|
||||
plugins: new Set(),
|
||||
},
|
||||
...children,
|
||||
],
|
||||
plugins: backstagePlugin ? new Set([backstagePlugin]) : new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
const emptyLoader = () => Promise.resolve(React.createElement('div'));
|
||||
|
||||
@@ -120,14 +124,13 @@ describe('discovery', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const plugin = createPlugin({
|
||||
id: 'test',
|
||||
extensions,
|
||||
});
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions,
|
||||
}),
|
||||
],
|
||||
plugins: [plugin],
|
||||
});
|
||||
|
||||
const info = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
@@ -146,25 +149,26 @@ describe('discovery', () => {
|
||||
[ref4, undefined],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
// expect(routing.objects).toEqual([
|
||||
// routeObj(
|
||||
// 'foo',
|
||||
// [ref1],
|
||||
// [
|
||||
// routeObj(
|
||||
// 'bar/:id',
|
||||
// [ref2],
|
||||
// [routeObj('baz', [ref3], undefined, undefined, plugin)],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// routeObj('blop', [ref5], undefined, undefined, plugin),
|
||||
// ],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// routeObj('divsoup', [ref4], undefined, undefined, plugin),
|
||||
// ]);
|
||||
expect(info.routeObjects).toEqual([
|
||||
routeObj('nothing', []),
|
||||
routeObj(
|
||||
'foo',
|
||||
[ref1],
|
||||
[
|
||||
routeObj(
|
||||
'bar/:id',
|
||||
[ref2],
|
||||
[routeObj('baz', [ref3], undefined, undefined, expect.any(Object))],
|
||||
undefined,
|
||||
expect.any(Object),
|
||||
),
|
||||
routeObj('blop', [ref5], undefined, undefined, expect.any(Object)),
|
||||
],
|
||||
undefined,
|
||||
expect.any(Object),
|
||||
),
|
||||
routeObj('divsoup', [ref4], undefined, undefined, expect.any(Object)),
|
||||
]);
|
||||
});
|
||||
|
||||
// it('should handle all react router Route patterns', () => {
|
||||
|
||||
@@ -19,6 +19,19 @@ import { coreExtensionData } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInstance } from '../wiring/createExtensionInstance';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { BackstageRouteObject } from '../../../core-app-api/src/routing/types';
|
||||
import { toLegacyPlugin } from '../wiring/createApp';
|
||||
|
||||
// We always add a child that matches all subroutes but without any route refs. This makes
|
||||
// sure that we're always able to match each route no matter how deep the navigation goes.
|
||||
// The route resolver then takes care of selecting the most specific match in order to find
|
||||
// mount points that are as deep in the routing tree as possible.
|
||||
export const MATCH_ALL_ROUTE: BackstageRouteObject = {
|
||||
caseSensitive: false,
|
||||
path: '*',
|
||||
element: 'match-all', // These elements aren't used, so we add in a bit of debug information
|
||||
routeRefs: new Set(),
|
||||
plugins: new Set(),
|
||||
};
|
||||
|
||||
// Joins a list of paths together, avoiding trailing and duplicate slashes
|
||||
export function joinPaths(...paths: string[]): string {
|
||||
@@ -36,10 +49,30 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
|
||||
} {
|
||||
const routePaths = new Map<RouteRef, string>();
|
||||
const routeParents = new Map<RouteRef, RouteRef | undefined>();
|
||||
const routeObjects = new Array<BackstageRouteObject>();
|
||||
|
||||
function visit(current: ExtensionInstance, parent?: RouteRef) {
|
||||
const routePath = current.getData(coreExtensionData.routePath) ?? '';
|
||||
function visit(
|
||||
current: ExtensionInstance,
|
||||
parentRef?: RouteRef,
|
||||
parentObj?: BackstageRouteObject,
|
||||
) {
|
||||
const routePath = current.getData(coreExtensionData.routePath) ?? ''; // TODO: need to gather routes instead
|
||||
const routeRef = current.getData(coreExtensionData.routeRef);
|
||||
const parentChildren = parentObj?.children ?? routeObjects;
|
||||
let currentObj = parentObj;
|
||||
|
||||
if (routePath) {
|
||||
currentObj = {
|
||||
path: routePath,
|
||||
element: 'mounted',
|
||||
routeRefs: new Set<RouteRef>(),
|
||||
caseSensitive: false,
|
||||
children: [MATCH_ALL_ROUTE],
|
||||
plugins: new Set(),
|
||||
};
|
||||
|
||||
parentChildren.push(currentObj);
|
||||
}
|
||||
|
||||
if (routeRef) {
|
||||
const routeRefId = (routeRef as any).id; // TODO: properly
|
||||
@@ -49,12 +82,16 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
|
||||
);
|
||||
}
|
||||
routePaths.set(routeRef, routePath);
|
||||
routeParents.set(routeRef, parent);
|
||||
routeParents.set(routeRef, parentRef);
|
||||
currentObj?.routeRefs.add(routeRef);
|
||||
if (current.source) {
|
||||
currentObj?.plugins.add(toLegacyPlugin(current.source));
|
||||
}
|
||||
}
|
||||
|
||||
for (const children of current.attachments.values()) {
|
||||
for (const child of children) {
|
||||
visit(child, routeRef ?? parent);
|
||||
visit(child, routeRef ?? parentRef, currentObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,5 +100,5 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
|
||||
visit(root);
|
||||
}
|
||||
|
||||
return { routePaths, routeParents, routeObjects: [] };
|
||||
return { routePaths, routeParents, routeObjects };
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ export function createApp(options: {
|
||||
};
|
||||
}
|
||||
|
||||
function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin {
|
||||
export function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin {
|
||||
const errorMsg = 'Not implemented in legacy plugin compatibility layer';
|
||||
const notImplemented = () => {
|
||||
throw new Error(errorMsg);
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface ExtensionInstance {
|
||||
* Maps input names to the actual instances given to them.
|
||||
*/
|
||||
readonly attachments: Map<string, ExtensionInstance[]>;
|
||||
|
||||
readonly source?: BackstagePlugin;
|
||||
}
|
||||
|
||||
function resolveInputData(
|
||||
@@ -141,7 +143,7 @@ export function createExtensionInstance(options: {
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
return extensionData.get(ref.id) as T | undefined;
|
||||
},
|
||||
|
||||
source,
|
||||
attachments,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user