Implemented a new 'getNodesByRoutePath' method in the AppTreeApi

Signed-off-by: Musaab Elfaqih <musaabe@spotify.com>
This commit is contained in:
Musaab Elfaqih
2025-01-13 16:02:50 +01:00
parent 3413feed07
commit 3e21b8d3dd
5 changed files with 68 additions and 18 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/frontend-app-api': patch
---
Added 'getNodesByRoutePath' method to the AppTreeApi.
@@ -31,6 +31,7 @@ import {
RouteResolutionApi,
createApiFactory,
routeResolutionApiRef,
AppNode,
} from '@backstage/frontend-plugin-api';
import {
AnyApiFactory,
@@ -68,7 +69,8 @@ import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
import { BackstageRouteObject } from '../routing/types';
import { FrontendFeature } from './types';
import { FrontendFeature, RouteInfo } from './types';
import { matchRoutes } from 'react-router-dom';
function deduplicateFeatures(
allFeatures: FrontendFeature[],
@@ -95,21 +97,47 @@ function deduplicateFeatures(
// Helps delay callers from reaching out to the API before the app tree has been materialized
class AppTreeApiProxy implements AppTreeApi {
#safeToUse: boolean = false;
#routeInfo?: RouteInfo;
constructor(private readonly tree: AppTree) {}
constructor(
private readonly tree: AppTree,
private readonly appBasePath: string,
) {}
getTree() {
if (!this.#safeToUse) {
private checkIfInitialized() {
if (!this.#routeInfo) {
throw new Error(
`You can't access the AppTreeApi during initialization of the app tree. Please move occurrences of this out of the initialization of the factory`,
);
}
}
getTree() {
this.checkIfInitialized();
return { tree: this.tree };
}
initialize() {
this.#safeToUse = true;
getNodesByRoutePath(sourcePath: string): { nodes: AppNode[] } {
this.checkIfInitialized();
let path = sourcePath;
if (sourcePath.startsWith(this.appBasePath)) {
path = sourcePath.slice(this.appBasePath.length);
}
const matchedRoutes = matchRoutes(this.#routeInfo!.routeObjects, path);
const matchedAppNodes =
matchedRoutes
?.filter(routeObj => !!routeObj.route.appNode)
.map(routeObj => routeObj.route.appNode!) || [];
return { nodes: matchedAppNodes };
}
initialize(routeInfo: RouteInfo) {
this.#routeInfo = routeInfo;
}
}
@@ -119,12 +147,11 @@ class RouteResolutionApiProxy implements RouteResolutionApi {
#routeObjects: BackstageRouteObject[] | undefined;
constructor(
private readonly tree: AppTree,
private readonly routeBindings: Map<
ExternalRouteRef,
RouteRef | SubRouteRef
>,
private readonly basePath: string,
private readonly appBasePath: string,
) {}
resolve<TParams extends AnyRouteRefParams>(
@@ -143,15 +170,13 @@ class RouteResolutionApiProxy implements RouteResolutionApi {
return this.#delegate.resolve(anyRouteRef, options);
}
initialize() {
const routeInfo = extractRouteInfoFromAppNode(this.tree.root);
initialize(routeInfo: RouteInfo) {
this.#delegate = new RouteResolver(
routeInfo.routePaths,
routeInfo.routeParents,
routeInfo.routeObjects,
this.routeBindings,
this.basePath,
this.appBasePath,
);
this.#routeObjects = routeInfo.routeObjects;
@@ -190,15 +215,15 @@ export function createSpecializedApp(options?: {
);
const factories = createApiFactories({ tree });
const appTreeApi = new AppTreeApiProxy(tree);
const appBasePath = getBasePath(config);
const appTreeApi = new AppTreeApiProxy(tree, appBasePath);
const routeResolutionApi = new RouteResolutionApiProxy(
tree,
resolveRouteBindings(
options?.bindRoutes,
config,
collectRouteIds(features),
),
getBasePath(config),
appBasePath,
);
const appIdentityProxy = new AppIdentityProxy();
@@ -237,8 +262,10 @@ export function createSpecializedApp(options?: {
// Now instantiate the entire tree, which will skip anything that's already been instantiated
instantiateAppNodeTree(tree.root, apiHolder);
routeResolutionApi.initialize();
appTreeApi.initialize();
const routeInfo = extractRouteInfoFromAppNode(tree.root);
routeResolutionApi.initialize(routeInfo);
appTreeApi.initialize(routeInfo);
const rootEl = tree.root.instance!.getData(coreExtensionData.reactElement);
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RouteRef } from '@backstage/frontend-plugin-api';
import { FrontendModule, FrontendPlugin } from '@backstage/frontend-plugin-api';
import { BackstageRouteObject } from '../routing/types';
/** @public */
export type FrontendFeature =
@@ -22,3 +24,10 @@ export type FrontendFeature =
// TODO(blam): This is just forwards backwards compatibility, remove after v1.31.0
| { $$type: '@backstage/ExtensionOverrides' }
| { $$type: '@backstage/BackstagePlugin' };
/** @internal */
export type RouteInfo = {
routePaths: Map<RouteRef, string>;
routeParents: Map<RouteRef, RouteRef | undefined>;
routeObjects: BackstageRouteObject[];
};
@@ -300,6 +300,9 @@ export interface AppTree {
// @public
export interface AppTreeApi {
getNodesByRoutePath(sourcePath: string): {
nodes: AppNode[];
};
getTree(): {
tree: AppTree;
};
@@ -104,6 +104,11 @@ export interface AppTreeApi {
* Get the {@link AppTree} for the app.
*/
getTree(): { tree: AppTree };
/**
* Get all nodes in the app that are mounted at a given route path.
*/
getNodesByRoutePath(sourcePath: string): { nodes: AppNode[] };
}
/**