diff --git a/.changeset/tame-hats-shout.md b/.changeset/tame-hats-shout.md new file mode 100644 index 0000000000..1576463c85 --- /dev/null +++ b/.changeset/tame-hats-shout.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-app-api': patch +--- + +Added `getNodesByRoutePath` method to the `AppTreeApi`. diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index e3f06a896d..5616346d75 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -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 (path.startsWith(this.appBasePath)) { + path = path.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( @@ -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); diff --git a/packages/frontend-app-api/src/wiring/types.ts b/packages/frontend-app-api/src/wiring/types.ts index fb887534e3..18d9c51d0e 100644 --- a/packages/frontend-app-api/src/wiring/types.ts +++ b/packages/frontend-app-api/src/wiring/types.ts @@ -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; + routeParents: Map; + routeObjects: BackstageRouteObject[]; +}; diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 24c2ff6d3b..6202eeaaa2 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -300,6 +300,9 @@ export interface AppTree { // @public export interface AppTreeApi { + getNodesByRoutePath(sourcePath: string): { + nodes: AppNode[]; + }; getTree(): { tree: AppTree; }; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts index 5f25eddead..6ced6706b9 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts @@ -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[] }; } /**