From 733bd95746b99ad8cdb4a7b87e8dc3e16d3b764a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Oct 2023 12:52:42 +0200 Subject: [PATCH] frontend-*-api: add and implement AppTreeApi Signed-off-by: Patrik Oldsberg --- .changeset/orange-jokes-tap.md | 5 ++ .changeset/shiny-goats-flash.md | 5 ++ .../routing/extractRouteInfoFromAppNode.ts | 2 +- .../src/tree/createAppTree.ts | 2 +- packages/frontend-app-api/src/tree/index.ts | 6 -- .../src/tree/instantiateAppNodeTree.test.ts | 2 +- .../src/tree/instantiateAppNodeTree.ts | 6 +- .../src/tree/resolveAppNodeSpecs.ts | 2 +- .../src/tree/resolveAppTree.ts | 7 ++- .../src/wiring/createApp.test.tsx | 57 +++++++++++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 21 +++++-- packages/frontend-plugin-api/api-report.md | 61 +++++++++++++++++++ .../src/apis/definitions/AppTreeApi.ts} | 55 ++++++++++------- .../src/apis/definitions/index.ts | 25 ++++++++ .../frontend-plugin-api/src/apis/index.ts | 17 ++++++ packages/frontend-plugin-api/src/index.ts | 1 + 16 files changed, 236 insertions(+), 38 deletions(-) create mode 100644 .changeset/orange-jokes-tap.md create mode 100644 .changeset/shiny-goats-flash.md rename packages/{frontend-app-api/src/tree/types.ts => frontend-plugin-api/src/apis/definitions/AppTreeApi.ts} (72%) create mode 100644 packages/frontend-plugin-api/src/apis/definitions/index.ts create mode 100644 packages/frontend-plugin-api/src/apis/index.ts diff --git a/.changeset/orange-jokes-tap.md b/.changeset/orange-jokes-tap.md new file mode 100644 index 0000000000..0fd9e14a46 --- /dev/null +++ b/.changeset/orange-jokes-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Implement new `AppTreeApi` diff --git a/.changeset/shiny-goats-flash.md b/.changeset/shiny-goats-flash.md new file mode 100644 index 0000000000..35a7ce0697 --- /dev/null +++ b/.changeset/shiny-goats-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Add new `AppTreeApi`. diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts index 5700bf4eae..43955328a8 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts @@ -18,7 +18,7 @@ import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toLegacyPlugin } from '../wiring/createApp'; import { BackstageRouteObject } from './types'; -import { AppNode } from '../tree'; +import { AppNode } from '@backstage/frontend-plugin-api'; // 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. diff --git a/packages/frontend-app-api/src/tree/createAppTree.ts b/packages/frontend-app-api/src/tree/createAppTree.ts index 2c36d56bf7..950669febd 100644 --- a/packages/frontend-app-api/src/tree/createAppTree.ts +++ b/packages/frontend-app-api/src/tree/createAppTree.ts @@ -22,7 +22,7 @@ import { import { readAppExtensionsConfig } from './readAppExtensionsConfig'; import { resolveAppTree } from './resolveAppTree'; import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; -import { AppTree } from './types'; +import { AppTree } from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; import { instantiateAppNodeTree } from './instantiateAppNodeTree'; diff --git a/packages/frontend-app-api/src/tree/index.ts b/packages/frontend-app-api/src/tree/index.ts index c864e86a7a..4b5ad5e867 100644 --- a/packages/frontend-app-api/src/tree/index.ts +++ b/packages/frontend-app-api/src/tree/index.ts @@ -14,10 +14,4 @@ * limitations under the License. */ -export type { - AppNode, - AppNodeEdges, - AppNodeInstance, - AppNodeSpec, -} from './types'; export { createAppTree } from './createAppTree'; diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 2f4a112166..7c8f49f96d 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -25,7 +25,7 @@ import { createAppNodeInstance, instantiateAppNodeTree, } from './instantiateAppNodeTree'; -import { AppNodeInstance, AppNodeSpec } from './types'; +import { AppNodeInstance, AppNodeSpec } from '@backstage/frontend-plugin-api'; import { resolveAppTree } from './resolveAppTree'; const testDataRef = createExtensionDataRef('test'); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index b6e4179867..53f6fdf4da 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -20,7 +20,11 @@ import { ExtensionDataRef, } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; -import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; +import { + AppNode, + AppNodeInstance, + AppNodeSpec, +} from '@backstage/frontend-plugin-api'; type Mutable = { -readonly [P in keyof T]: T[P]; diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index ca3c4fd1ea..2133bfc457 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -22,7 +22,7 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; import { ExtensionParameters } from './readAppExtensionsConfig'; -import { AppNodeSpec } from './types'; +import { AppNodeSpec } from '@backstage/frontend-plugin-api'; /** @internal */ export function resolveAppNodeSpecs(options: { diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.ts b/packages/frontend-app-api/src/tree/resolveAppTree.ts index e1665a01fd..24948373bf 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { AppTree, AppNode, AppNodeInstance, AppNodeSpec } from './types'; +import { + AppTree, + AppNode, + AppNodeInstance, + AppNodeSpec, +} from '@backstage/frontend-plugin-api'; function indent(str: string) { return str.replace(/^/gm, ' '); diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index e9fb48f957..80d9f846f0 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -15,6 +15,8 @@ */ import { + AppTreeApi, + appTreeApiRef, createPageExtension, createPlugin, createThemeExtension, @@ -23,6 +25,7 @@ import { screen, waitFor } from '@testing-library/react'; import { createApp } from './createApp'; import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; +import { useApi } from '@backstage/core-plugin-api'; describe('createApp', () => { it('should allow themes to be installed', async () => { @@ -90,4 +93,58 @@ describe('createApp', () => { expect(screen.getByText('Last Page')).toBeInTheDocument(), ); }); + + it('should make the app structure available through the AppTreeApi', async () => { + let appTreeApi: AppTreeApi | undefined = undefined; + + const app = createApp({ + configLoader: async () => new MockConfigApi({}), + features: [ + createPlugin({ + id: 'my-plugin', + extensions: [ + createPageExtension({ + id: 'plugin.my-plugin.page', + defaultPath: '/', + loader: async () => { + const Component = () => { + appTreeApi = useApi(appTreeApiRef); + return
My Plugin Page
; + }; + return ; + }, + }), + ], + }), + ], + }); + + await renderWithEffects(app.createRoot()); + + expect(appTreeApi).toBeDefined(); + const { tree } = appTreeApi!.getTree(); + + expect(String(tree.root)).toMatchInlineSnapshot(` + " + root [ + + content [ + + routes [ + + ] + + ] + nav [ + + ] + + ] + themes [ + + + ] + " + `); + }); }); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index d726773fd3..f02b440ff5 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -17,6 +17,8 @@ import React, { JSX } from 'react'; import { ConfigReader, Config } from '@backstage/config'; import { + AppTree, + appTreeApiRef, BackstagePlugin, coreExtensionData, ExtensionDataRef, @@ -87,7 +89,8 @@ import { AppRouteBinder } from '../routing'; import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; -import { AppNode, createAppTree } from '../tree'; +import { createAppTree } from '../tree'; +import { AppNode } from '@backstage/frontend-plugin-api'; const builtinExtensions = [ Core, @@ -262,7 +265,7 @@ export function createApp(options: { const routeIds = collectRouteIds(allFeatures); const App = () => ( - + e.instance?.getData(coreExtensionData.apiFactory)) .filter((x): x is AnyApiFactory => !!x) ?? []; const themeExtensions = - core.edges.attachments + tree.root.edges.attachments .get('themes') ?.map(e => e.instance?.getData(coreExtensionData.theme)) .filter((x): x is AppTheme => !!x) ?? []; @@ -414,6 +417,14 @@ function createApiHolder(core: AppNode, configApi: ConfigApi): ApiHolder { }, }); + factoryRegistry.register('static', { + api: appTreeApiRef, + deps: {}, + factory: () => ({ + getTree: () => ({ tree }), + }), + }); + factoryRegistry.register('static', { api: appThemeApiRef, deps: {}, diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 250ce5f676..794da9617b 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -7,6 +7,7 @@ import { AnyApiFactory } from '@backstage/core-plugin-api'; import { AnyApiRef } from '@backstage/core-plugin-api'; +import { ApiRef } from '@backstage/core-plugin-api'; import { AppTheme } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; @@ -55,6 +56,66 @@ export type AnyRoutes = { [name in string]: RouteRef; }; +// @public +export interface AppNode { + readonly edges: AppNodeEdges; + readonly instance?: AppNodeInstance; + readonly spec: AppNodeSpec; +} + +// @public +export interface AppNodeEdges { + // (undocumented) + readonly attachedTo?: { + node: AppNode; + input: string; + }; + // (undocumented) + readonly attachments: ReadonlyMap; +} + +// @public +export interface AppNodeInstance { + getData(ref: ExtensionDataRef): T | undefined; + getDataRefs(): Iterable>; +} + +// @public +export interface AppNodeSpec { + // (undocumented) + readonly attachTo: { + id: string; + input: string; + }; + // (undocumented) + readonly config?: unknown; + // (undocumented) + readonly disabled: boolean; + // (undocumented) + readonly extension: Extension; + // (undocumented) + readonly id: string; + // (undocumented) + readonly source?: BackstagePlugin; +} + +// @public +export interface AppTree { + readonly nodes: ReadonlyMap; + readonly orphans: Iterable; + readonly root: AppNode; +} + +// @public +export interface AppTreeApi { + getTree(): { + tree: AppTree; + }; +} + +// @public +export const appTreeApiRef: ApiRef; + // @public (undocumented) export interface BackstagePlugin< Routes extends AnyRoutes = AnyRoutes, diff --git a/packages/frontend-app-api/src/tree/types.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts similarity index 72% rename from packages/frontend-app-api/src/tree/types.ts rename to packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts index 3246dfe5c2..6c79a16964 100644 --- a/packages/frontend-app-api/src/tree/types.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts @@ -14,20 +14,13 @@ * limitations under the License. */ -import { - BackstagePlugin, - Extension, - ExtensionDataRef, -} from '@backstage/frontend-plugin-api'; - -/* -NOTE: These types are marked as @internal for now, but the intention is for this to be a public API in the future. -*/ +import { createApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin, Extension, ExtensionDataRef } from '../../wiring'; /** - * The specification for this node in the app tree. + * The specification for this {@link AppNode} in the {@link AppTree}. * - * @internal + * @public * @remarks * * The specifications for a collection of app nodes is all the information needed @@ -43,9 +36,9 @@ export interface AppNodeSpec { } /** - * The connections from this node to other nodes. + * The connections from this {@link AppNode} to other nodes. * - * @internal + * @public * @remarks * * The app node edges are resolved based on the app node specs, regardless of whether @@ -57,9 +50,9 @@ export interface AppNodeEdges { } /** - * The instance of this node in the app tree. + * The instance of this {@link AppNode} in the {@link AppTree}. * - * @internal + * @public * @remarks * * The app node instance is created when the `factory` function of an extension is called. @@ -74,8 +67,9 @@ export interface AppNodeInstance { } /** + * A node in the {@link AppTree}. * - * @internal + * @public */ export interface AppNode { /** The specification for how this node should be instantiated */ @@ -87,15 +81,34 @@ export interface AppNode { } /** - * The app tree containing all nodes of the app. + * The app tree containing all {@link AppNode}s of the app. * - * @internal + * @public */ export interface AppTree { /** The root node of the app */ - root: AppNode; + readonly root: AppNode; /** A map of all nodes in the app by ID, including orphaned or disabled nodes */ - nodes: ReadonlyMap; + readonly nodes: ReadonlyMap; /** A sequence of all nodes with a parent that is not reachable from the app root node */ - orphans: Iterable; + readonly orphans: Iterable; } + +/** + * The API for interacting with the {@link AppTree}. + * + * @public + */ +export interface AppTreeApi { + /** + * Get the {@link AppTree} for the app. + */ + getTree(): { tree: AppTree }; +} + +/** + * The `ApiRef` of {@link AppTreeApi}. + * + * @public + */ +export const appTreeApiRef = createApiRef({ id: 'core.app-tree' }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts new file mode 100644 index 0000000000..8facdca2ca --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export { + appTreeApiRef, + type AppNode, + type AppNodeEdges, + type AppNodeInstance, + type AppNodeSpec, + type AppTree, + type AppTreeApi, +} from './AppTreeApi'; diff --git a/packages/frontend-plugin-api/src/apis/index.ts b/packages/frontend-plugin-api/src/apis/index.ts new file mode 100644 index 0000000000..5a012c0553 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './definitions'; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 73abf28ce4..498bc96a77 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './apis'; export * from './components'; export * from './extensions'; export * from './routing';