Merge pull request #20341 from backstage/mob/more-routing
frontend-app-api: add support for existing routing system
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Added support for the existing routing system.
|
||||
@@ -42,6 +42,7 @@
|
||||
"@backstage/plugin-graphiql": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
createRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { extractRouteInfoFromInstanceTree } from './extractRouteInfoFromInstanceTree';
|
||||
import {
|
||||
Extension,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createInstances } from '../wiring/createApp';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
|
||||
const ref1 = createRouteRef({ id: 'page1' });
|
||||
const ref2 = createRouteRef({ id: 'page2' });
|
||||
const ref3 = createRouteRef({ id: 'page3' });
|
||||
const ref4 = createRouteRef({ id: 'page4' });
|
||||
const ref5 = createRouteRef({ id: 'page5' });
|
||||
const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
function createTestExtension(options: {
|
||||
id: string;
|
||||
at?: string;
|
||||
path?: string;
|
||||
routeRef?: RouteRef;
|
||||
}) {
|
||||
return createExtension({
|
||||
id: options.id,
|
||||
at: options.at ?? 'core.routes/children',
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath.optional(),
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
},
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({
|
||||
path: options.path,
|
||||
routeRef: options.routeRef,
|
||||
element: React.createElement('div'),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
|
||||
const plugin = createPlugin({
|
||||
id: 'test',
|
||||
extensions,
|
||||
});
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [plugin],
|
||||
});
|
||||
|
||||
return extractRouteInfoFromInstanceTree(rootInstances);
|
||||
}
|
||||
|
||||
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
|
||||
return Array.from(map).sort(
|
||||
([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b),
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('discovery', () => {
|
||||
it('should collect routes', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'nothing',
|
||||
path: 'nothing',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
path: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/children',
|
||||
path: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/children',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
path: 'divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page1/children',
|
||||
path: 'blop',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'foo'],
|
||||
[ref2, 'bar/:id'],
|
||||
[ref3, 'baz'],
|
||||
[ref4, 'divsoup'],
|
||||
[ref5, 'blop'],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref2],
|
||||
[ref4, undefined],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
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', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
path: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/children',
|
||||
path: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3/children',
|
||||
path: 'divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page3/children',
|
||||
path: 'blop',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'foo'],
|
||||
[ref2, 'bar/:id'],
|
||||
[ref3, 'baz'],
|
||||
[ref4, 'divsoup'],
|
||||
[ref5, 'blop'],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should strip leading slashes in route paths', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
path: '/foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/children',
|
||||
path: '/bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
path: '/baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3/children',
|
||||
path: '/divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page3/children',
|
||||
path: '/blop',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'foo'],
|
||||
[ref2, 'bar/:id'],
|
||||
[ref3, 'baz'],
|
||||
[ref4, 'divsoup'],
|
||||
[ref5, 'blop'],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the route aggregator key to bind child routes to the same path', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'foo',
|
||||
path: 'foo',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
at: 'foo/children',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'fooChild',
|
||||
at: 'foo/children',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'fooChild/children',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'fooEmpty',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
path: 'bar',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3Child',
|
||||
at: 'page3/children',
|
||||
path: '',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3Child/children',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page4/children',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'foo'],
|
||||
[ref2, 'foo'],
|
||||
[ref3, 'bar'],
|
||||
[ref4, ''],
|
||||
[ref5, ''],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, undefined],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
expect(info.routeObjects).toEqual([
|
||||
routeObj('foo', [ref1, ref2], [], 'mounted', expect.any(Object)),
|
||||
routeObj(
|
||||
'bar',
|
||||
[ref3],
|
||||
[routeObj('', [ref4, ref5], [], 'mounted', expect.any(Object))],
|
||||
'mounted',
|
||||
expect.any(Object),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the route aggregator but stop when encountering explicit path', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
path: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1Child',
|
||||
at: 'page1/children',
|
||||
path: 'bar',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1Child/children',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/children',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3/children',
|
||||
path: '/blop',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page2/children',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'foo'],
|
||||
[ref2, 'bar'],
|
||||
[ref3, 'baz'],
|
||||
[ref4, 'blop'],
|
||||
[ref5, 'bar'],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref2],
|
||||
[ref4, ref3],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
expect(info.routeObjects).toEqual([
|
||||
routeObj(
|
||||
'foo',
|
||||
[ref1],
|
||||
[
|
||||
routeObj(
|
||||
'bar',
|
||||
[ref2, ref5],
|
||||
[
|
||||
routeObj(
|
||||
'baz',
|
||||
[ref3],
|
||||
[
|
||||
routeObj(
|
||||
'blop',
|
||||
[ref4],
|
||||
undefined,
|
||||
undefined,
|
||||
expect.any(Object),
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
expect.any(Object),
|
||||
),
|
||||
],
|
||||
'mounted',
|
||||
expect.any(Object),
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
expect.any(Object),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should account for loose route paths', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'r',
|
||||
path: 'r',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
at: 'r/children',
|
||||
path: 'x',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'y',
|
||||
path: 'y',
|
||||
at: 'r/children',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'y/children',
|
||||
path: '1',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/children',
|
||||
path: 'a',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page2/children',
|
||||
path: 'b',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'r/x'],
|
||||
[ref2, 'r/y/1'],
|
||||
[ref3, 'a'],
|
||||
[ref4, 'b'],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, undefined],
|
||||
[ref3, ref2],
|
||||
[ref4, ref2],
|
||||
]);
|
||||
expect(info.routeObjects).toEqual([
|
||||
routeObj(
|
||||
'r',
|
||||
[],
|
||||
[
|
||||
routeObj('x', [ref1], [], 'mounted', expect.any(Object)),
|
||||
routeObj(
|
||||
'y',
|
||||
[],
|
||||
[
|
||||
routeObj(
|
||||
'1',
|
||||
[ref2],
|
||||
[
|
||||
routeObj(
|
||||
'a',
|
||||
[ref3],
|
||||
undefined,
|
||||
'mounted',
|
||||
expect.any(Object),
|
||||
),
|
||||
routeObj(
|
||||
'b',
|
||||
[ref4],
|
||||
undefined,
|
||||
'mounted',
|
||||
expect.any(Object),
|
||||
),
|
||||
],
|
||||
'mounted',
|
||||
expect.any(Object),
|
||||
),
|
||||
],
|
||||
'mounted',
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
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 {
|
||||
const normalized = paths.join('/').replace(/\/\/+/g, '/');
|
||||
if (normalized !== '/' && normalized.endsWith('/')) {
|
||||
return normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
routeObjects: BackstageRouteObject[];
|
||||
} {
|
||||
// This tracks the route path for each route ref, the value is the route path relative to the parent ref
|
||||
const routePaths = new Map<RouteRef, string>();
|
||||
// This tracks the parents of each route ref. To find the full path of any route ref you traverse
|
||||
// upwards in this tree and substitute each route ref for its route path along then way.
|
||||
const routeParents = new Map<RouteRef, RouteRef | undefined>();
|
||||
// This route object tree is passed to react-router in order to be able to look up the current route
|
||||
// ref or extension/source based on our current location.
|
||||
const routeObjects = new Array<BackstageRouteObject>();
|
||||
|
||||
function visit(
|
||||
current: ExtensionInstance,
|
||||
collectedPath?: string,
|
||||
foundRefForCollectedPath: boolean = false,
|
||||
parentRef?: RouteRef,
|
||||
candidateParentRef?: RouteRef,
|
||||
parentObj?: BackstageRouteObject,
|
||||
) {
|
||||
const routePath = current
|
||||
.getData(coreExtensionData.routePath)
|
||||
?.replace(/^\//, '');
|
||||
const routeRef = current.getData(coreExtensionData.routeRef);
|
||||
const parentChildren = parentObj?.children ?? routeObjects;
|
||||
let currentObj = parentObj;
|
||||
|
||||
let newCollectedPath = collectedPath;
|
||||
let newFoundRefForCollectedPath = foundRefForCollectedPath;
|
||||
|
||||
let newParentRef = parentRef;
|
||||
let newCandidateParentRef = candidateParentRef;
|
||||
|
||||
// Whenever a route path is encountered, a new node is created in the routing tree.
|
||||
if (routePath !== undefined) {
|
||||
currentObj = {
|
||||
path: routePath,
|
||||
element: 'mounted',
|
||||
routeRefs: new Set<RouteRef>(),
|
||||
caseSensitive: false,
|
||||
children: [MATCH_ALL_ROUTE],
|
||||
plugins: new Set(),
|
||||
};
|
||||
parentChildren.push(currentObj);
|
||||
|
||||
// Each route path that we discover creates a new node in the routing tree, at that point
|
||||
// we also switch out our candidate parent ref to be the active one.
|
||||
newParentRef = candidateParentRef;
|
||||
newCandidateParentRef = undefined;
|
||||
|
||||
// We need to collect and concatenate route paths until the path has been assigned a route ref:
|
||||
// Once we find a route ref the collection starts over from an empty path, that way each route
|
||||
// path assignment only contains the diff from the parent ref.
|
||||
if (newFoundRefForCollectedPath) {
|
||||
newCollectedPath = routePath;
|
||||
newFoundRefForCollectedPath = false;
|
||||
} else {
|
||||
newCollectedPath = collectedPath
|
||||
? joinPaths(collectedPath, routePath)
|
||||
: routePath;
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever a route ref is encountered, we need to give it a route path and position in the ref tree.
|
||||
if (routeRef) {
|
||||
const routeRefId = (routeRef as any).id; // TODO: properly
|
||||
if (routeRefId !== current.id) {
|
||||
throw new Error(
|
||||
`Route ref '${routeRefId}' must have the same ID as extension '${current.id}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// The first route ref we find after encountering a route path is selected to be used as the
|
||||
// parent ref further down the tree. We don't start using this candidate ref until we encounter
|
||||
// another route path though, at which point we repeat the process and select another candidate.
|
||||
if (!newCandidateParentRef) {
|
||||
newCandidateParentRef = routeRef;
|
||||
}
|
||||
|
||||
// Check if we've encountered any route paths since the closest route ref, in that case we assign
|
||||
// that path to this and following route refs until we encounter another route path.
|
||||
if (newCollectedPath !== undefined) {
|
||||
routePaths.set(routeRef, newCollectedPath);
|
||||
newFoundRefForCollectedPath = true;
|
||||
}
|
||||
|
||||
routeParents.set(routeRef, newParentRef);
|
||||
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,
|
||||
newCollectedPath,
|
||||
newFoundRefForCollectedPath,
|
||||
newParentRef,
|
||||
newCandidateParentRef,
|
||||
currentObj,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
visit(root);
|
||||
}
|
||||
|
||||
return { routePaths, routeParents, routeObjects };
|
||||
}
|
||||
@@ -74,6 +74,8 @@ import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultCo
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { RoutingProvider as LegacyRoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
apis as defaultApis,
|
||||
components as defaultComponents,
|
||||
@@ -82,6 +84,8 @@ import {
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import { DarkTheme, LightTheme } from '../extensions/themes';
|
||||
import { extractRouteInfoFromInstanceTree } from '../routing/extractRouteInfoFromInstanceTree';
|
||||
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionTreeNode {
|
||||
@@ -281,7 +285,7 @@ export function createApp(options: {
|
||||
config,
|
||||
});
|
||||
|
||||
const routePaths = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
const routeInfo = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
|
||||
const coreInstance = rootInstances.find(({ id }) => id === 'core');
|
||||
if (!coreInstance) {
|
||||
@@ -304,10 +308,15 @@ export function createApp(options: {
|
||||
<ApiProvider apis={apiHolder}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<AppThemeProvider>
|
||||
<RoutingProvider routePaths={routePaths}>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>{rootElements}</BrowserRouter>
|
||||
</RoutingProvider>
|
||||
<LegacyRoutingProvider
|
||||
{...routeInfo}
|
||||
routeBindings={new Map(/* TODO */)}
|
||||
>
|
||||
<RoutingProvider routePaths={routeInfo.routePaths}>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>{rootElements}</BrowserRouter>
|
||||
</RoutingProvider>
|
||||
</LegacyRoutingProvider>
|
||||
</AppThemeProvider>
|
||||
</AppContextProvider>
|
||||
</ApiProvider>
|
||||
@@ -328,25 +337,40 @@ export function createApp(options: {
|
||||
};
|
||||
}
|
||||
|
||||
function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin {
|
||||
// Make sure that we only convert each new plugin instance to its legacy equivalent once
|
||||
const legacyPluginStore = getOrCreateGlobalSingleton(
|
||||
'legacy-plugin-compatibility-store',
|
||||
() => new WeakMap<BackstagePlugin, LegacyBackstagePlugin>(),
|
||||
);
|
||||
|
||||
export function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin {
|
||||
let legacy = legacyPluginStore.get(plugin);
|
||||
if (legacy) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
const errorMsg = 'Not implemented in legacy plugin compatibility layer';
|
||||
const notImplemented = () => {
|
||||
throw new Error(errorMsg);
|
||||
};
|
||||
return {
|
||||
|
||||
legacy = {
|
||||
getId(): string {
|
||||
return plugin.id;
|
||||
},
|
||||
get routes(): never {
|
||||
throw new Error(errorMsg);
|
||||
get routes() {
|
||||
return {};
|
||||
},
|
||||
get externalRoutes(): never {
|
||||
throw new Error(errorMsg);
|
||||
get externalRoutes() {
|
||||
return {};
|
||||
},
|
||||
getApis: notImplemented,
|
||||
getFeatureFlags: notImplemented,
|
||||
provide: notImplemented,
|
||||
};
|
||||
|
||||
legacyPluginStore.set(plugin, legacy);
|
||||
return legacy;
|
||||
}
|
||||
|
||||
function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext {
|
||||
@@ -458,38 +482,3 @@ function createApiHolder(
|
||||
|
||||
return new ApiResolver(factoryRegistry);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function extractRouteInfoFromInstanceTree(
|
||||
roots: ExtensionInstance[],
|
||||
): Map<RouteRef, string> {
|
||||
const results = new Map<RouteRef, string>();
|
||||
|
||||
function visit(current: ExtensionInstance, basePath: string) {
|
||||
const routePath = current.getData(coreExtensionData.routePath) ?? '';
|
||||
const routeRef = current.getData(coreExtensionData.routeRef);
|
||||
|
||||
// TODO: join paths in a more robust way
|
||||
const fullPath = basePath + routePath;
|
||||
if (routeRef) {
|
||||
const routeRefId = (routeRef as any).id; // TODO: properly
|
||||
if (routeRefId !== current.id) {
|
||||
throw new Error(
|
||||
`Route ref '${routeRefId}' must have the same ID as extension '${current.id}'`,
|
||||
);
|
||||
}
|
||||
results.set(routeRef, fullPath);
|
||||
}
|
||||
|
||||
for (const children of current.attachments.values()) {
|
||||
for (const child of children) {
|
||||
visit(child, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
visit(root, '');
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4313,6 +4313,7 @@ __metadata:
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@material-ui/core": ^4.12.4
|
||||
"@material-ui/icons": ^4.11.3
|
||||
"@testing-library/jest-dom": ^5.10.1
|
||||
|
||||
Reference in New Issue
Block a user