frontend-*-api: add and implement AppTreeApi
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Implement new `AppTreeApi`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Add new `AppTreeApi`.
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -14,10 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
AppNode,
|
||||
AppNodeEdges,
|
||||
AppNodeInstance,
|
||||
AppNodeSpec,
|
||||
} from './types';
|
||||
export { createAppTree } from './createAppTree';
|
||||
|
||||
@@ -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<string>('test');
|
||||
|
||||
@@ -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<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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, ' ');
|
||||
|
||||
@@ -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 <div>My Plugin Page</div>;
|
||||
};
|
||||
return <Component />;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
expect(appTreeApi).toBeDefined();
|
||||
const { tree } = appTreeApi!.getTree();
|
||||
|
||||
expect(String(tree.root)).toMatchInlineSnapshot(`
|
||||
"<core out=[core.reactElement]>
|
||||
root [
|
||||
<core.layout out=[core.reactElement]>
|
||||
content [
|
||||
<core.routes out=[core.reactElement]>
|
||||
routes [
|
||||
<plugin.my-plugin.page out=[core.routing.path, core.routing.ref, core.reactElement] />
|
||||
]
|
||||
</core.routes>
|
||||
]
|
||||
nav [
|
||||
<core.nav out=[core.reactElement] />
|
||||
]
|
||||
</core.layout>
|
||||
]
|
||||
themes [
|
||||
<themes.light out=[core.theme] />
|
||||
<themes.dark out=[core.theme] />
|
||||
]
|
||||
</core>"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = () => (
|
||||
<ApiProvider apis={createApiHolder(tree.root, config)}>
|
||||
<ApiProvider apis={createApiHolder(tree, config)}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<AppThemeProvider>
|
||||
<RoutingProvider
|
||||
@@ -356,17 +359,17 @@ function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext {
|
||||
};
|
||||
}
|
||||
|
||||
function createApiHolder(core: AppNode, configApi: ConfigApi): ApiHolder {
|
||||
function createApiHolder(tree: AppTree, configApi: ConfigApi): ApiHolder {
|
||||
const factoryRegistry = new ApiFactoryRegistry();
|
||||
|
||||
const pluginApis =
|
||||
core.edges.attachments
|
||||
tree.root.edges.attachments
|
||||
.get('apis')
|
||||
?.map(e => 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: {},
|
||||
|
||||
@@ -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<string, AppNode[]>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface AppNodeInstance {
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined;
|
||||
getDataRefs(): Iterable<ExtensionDataRef<unknown>>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface AppNodeSpec {
|
||||
// (undocumented)
|
||||
readonly attachTo: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
// (undocumented)
|
||||
readonly config?: unknown;
|
||||
// (undocumented)
|
||||
readonly disabled: boolean;
|
||||
// (undocumented)
|
||||
readonly extension: Extension<unknown>;
|
||||
// (undocumented)
|
||||
readonly id: string;
|
||||
// (undocumented)
|
||||
readonly source?: BackstagePlugin;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface AppTree {
|
||||
readonly nodes: ReadonlyMap<string, AppNode>;
|
||||
readonly orphans: Iterable<AppNode>;
|
||||
readonly root: AppNode;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface AppTreeApi {
|
||||
getTree(): {
|
||||
tree: AppTree;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export const appTreeApiRef: ApiRef<AppTreeApi>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BackstagePlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
|
||||
+34
-21
@@ -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<string /* id */, AppNode>;
|
||||
readonly nodes: ReadonlyMap<string /* id */, AppNode>;
|
||||
/** A sequence of all nodes with a parent that is not reachable from the app root node */
|
||||
orphans: Iterable<AppNode>;
|
||||
readonly orphans: Iterable<AppNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<AppTreeApi>({ id: 'core.app-tree' });
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -20,6 +20,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './apis';
|
||||
export * from './components';
|
||||
export * from './extensions';
|
||||
export * from './routing';
|
||||
|
||||
Reference in New Issue
Block a user