Merge pull request #21471 from backstage/rugvip/node
frontend-plugin-api: change extension factory to receive app node instead of id and source
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Internal refactor of alpha exports due to a change in how extension factories are defined.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Updates to provide `node` to extension factories instead of `id` and `source`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': minor
|
||||
---
|
||||
|
||||
The extension `factory` function now longer receives `id` or `source`, but instead now provides the extension's `AppNode` as `node`. The `ExtensionBoundary` component has also been updated to receive a `node` prop rather than `id` and `source`.
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AppNode,
|
||||
Extension,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
@@ -52,15 +53,27 @@ const simpleExtension = createExtension({
|
||||
|
||||
function makeSpec<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
config?: TConfig,
|
||||
spec?: Partial<AppNodeSpec>,
|
||||
): AppNodeSpec {
|
||||
return {
|
||||
id: extension.id,
|
||||
attachTo: extension.attachTo,
|
||||
disabled: extension.disabled,
|
||||
extension,
|
||||
config,
|
||||
source: undefined,
|
||||
...spec,
|
||||
};
|
||||
}
|
||||
|
||||
function makeNode<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
spec?: Partial<AppNodeSpec>,
|
||||
): AppNode {
|
||||
return {
|
||||
spec: makeSpec(extension, spec),
|
||||
edges: {
|
||||
attachments: new Map(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,7 +84,7 @@ function makeInstanceWithId<TConfig>(
|
||||
return {
|
||||
id: extension.id,
|
||||
instance: createAppNodeInstance({
|
||||
spec: makeSpec(extension, config),
|
||||
node: makeNode(extension, { config }),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
};
|
||||
@@ -80,7 +93,7 @@ function makeInstanceWithId<TConfig>(
|
||||
describe('instantiateAppNodeTree', () => {
|
||||
it('should instantiate a single node', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
{ ...makeSpec(simpleExtension), id: 'root-node' },
|
||||
makeSpec(simpleExtension, { id: 'root-node' }),
|
||||
]);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
@@ -94,7 +107,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
it('should not instantiate disabled nodes', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true },
|
||||
makeSpec(simpleExtension, { id: 'root-node', disabled: true }),
|
||||
]);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
@@ -103,28 +116,25 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
it('should instantiate a node with attachments', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
{
|
||||
...makeSpec(
|
||||
createExtension({
|
||||
id: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
...makeSpec(simpleExtension),
|
||||
makeSpec(
|
||||
createExtension({
|
||||
id: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
makeSpec(simpleExtension, {
|
||||
id: 'child-node',
|
||||
attachTo: { id: 'root-node', input: 'test' },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const childNode = tree.nodes.get('child-node');
|
||||
@@ -191,7 +201,7 @@ describe('createAppNodeInstance', () => {
|
||||
it('should create a simple extension instance', () => {
|
||||
const attachments = new Map();
|
||||
const instance = createAppNodeInstance({
|
||||
spec: makeSpec(simpleExtension),
|
||||
node: makeNode(simpleExtension),
|
||||
attachments,
|
||||
});
|
||||
|
||||
@@ -231,7 +241,7 @@ describe('createAppNodeInstance', () => {
|
||||
]);
|
||||
const instance = createAppNodeInstance({
|
||||
attachments,
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -283,10 +293,7 @@ describe('createAppNodeInstance', () => {
|
||||
it('should refuse to create an extension with invalid config', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: {
|
||||
...makeSpec(simpleExtension),
|
||||
config: { other: 'not-a-number' },
|
||||
},
|
||||
node: makeNode(simpleExtension, { config: { other: 'not-a-number' } }),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
@@ -297,7 +304,7 @@ describe('createAppNodeInstance', () => {
|
||||
it('should forward extension factory errors', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -319,7 +326,7 @@ describe('createAppNodeInstance', () => {
|
||||
it('should refuse to create an instance with duplicate output', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -342,7 +349,7 @@ describe('createAppNodeInstance', () => {
|
||||
it('should refuse to create an instance with disconnected output data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -364,7 +371,7 @@ describe('createAppNodeInstance', () => {
|
||||
it('should refuse to create an instance with missing required input', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -408,7 +415,7 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -443,7 +450,7 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -469,7 +476,7 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -503,7 +510,7 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
@@ -531,7 +538,7 @@ describe('createAppNodeInstance', () => {
|
||||
attachments: new Map([
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
|
||||
@@ -20,11 +20,7 @@ import {
|
||||
ExtensionDataRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import {
|
||||
AppNode,
|
||||
AppNodeInstance,
|
||||
AppNodeSpec,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
@@ -99,11 +95,11 @@ function resolveInputs(
|
||||
|
||||
/** @internal */
|
||||
export function createAppNodeInstance(options: {
|
||||
spec: AppNodeSpec;
|
||||
node: AppNode;
|
||||
attachments: ReadonlyMap<string, { id: string; instance: AppNodeInstance }[]>;
|
||||
}): AppNodeInstance {
|
||||
const { spec, attachments } = options;
|
||||
const { id, extension, config, source } = spec;
|
||||
const { node, attachments } = options;
|
||||
const { id, extension, config } = node.spec;
|
||||
const extensionData = new Map<string, unknown>();
|
||||
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
|
||||
|
||||
@@ -118,7 +114,7 @@ export function createAppNodeInstance(options: {
|
||||
|
||||
try {
|
||||
const namedOutputs = extension.factory({
|
||||
source,
|
||||
node,
|
||||
config: parsedConfig,
|
||||
inputs: resolveInputs(extension.inputs, attachments),
|
||||
});
|
||||
@@ -186,7 +182,7 @@ export function instantiateAppNodeTree(rootNode: AppNode): void {
|
||||
}
|
||||
|
||||
(node as Mutable<AppNode>).instance = createAppNodeInstance({
|
||||
spec: node.spec,
|
||||
node,
|
||||
attachments: instantiatedAttachments,
|
||||
});
|
||||
|
||||
|
||||
@@ -339,7 +339,7 @@ export interface CreateExtensionOptions<
|
||||
disabled?: boolean;
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
@@ -493,7 +493,7 @@ export interface Extension<TConfig> {
|
||||
disabled: boolean;
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
@@ -518,11 +518,9 @@ export interface ExtensionBoundaryProps {
|
||||
// (undocumented)
|
||||
children: ReactNode;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
node: AppNode;
|
||||
// (undocumented)
|
||||
routable?: boolean;
|
||||
// (undocumented)
|
||||
source?: BackstagePlugin;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -34,15 +34,11 @@ const wrapInBoundaryExtension = (element: JSX.Element) => {
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
},
|
||||
factory({ source }) {
|
||||
factory({ node }) {
|
||||
return {
|
||||
routeRef,
|
||||
path: '/',
|
||||
element: (
|
||||
<ExtensionBoundary id={id} source={source}>
|
||||
{element}
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
element: <ExtensionBoundary node={node}>{element}</ExtensionBoundary>,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import React, { PropsWithChildren, ReactNode, useEffect } from 'react';
|
||||
import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '../wiring';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { ExtensionSuspense } from './ExtensionSuspense';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker';
|
||||
import { AppNode } from '../apis';
|
||||
|
||||
type RouteTrackerProps = PropsWithChildren<{
|
||||
disableTracking?: boolean;
|
||||
@@ -44,25 +44,24 @@ const RouteTracker = (props: RouteTrackerProps) => {
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionBoundaryProps {
|
||||
id: string;
|
||||
source?: BackstagePlugin;
|
||||
node: AppNode;
|
||||
routable?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function ExtensionBoundary(props: ExtensionBoundaryProps) {
|
||||
const { id, source, routable, children } = props;
|
||||
const { node, routable, children } = props;
|
||||
|
||||
// Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight
|
||||
const attributes = {
|
||||
extension: id,
|
||||
pluginId: source?.id,
|
||||
extension: node.spec.id,
|
||||
pluginId: node.spec.source?.id,
|
||||
};
|
||||
|
||||
return (
|
||||
<ExtensionSuspense>
|
||||
<ErrorBoundary plugin={source}>
|
||||
<ErrorBoundary plugin={node.spec.source}>
|
||||
<AnalyticsContext attributes={attributes}>
|
||||
<RouteTracker disableTracking={!routable}>{children}</RouteTracker>
|
||||
</AnalyticsContext>
|
||||
|
||||
@@ -15,16 +15,12 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { PortableSchema } from '../schema';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
createPlugin,
|
||||
} from '../wiring';
|
||||
import { coreExtensionData, createExtensionInput } from '../wiring';
|
||||
import { createPageExtension } from './createPageExtension';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
@@ -120,19 +116,13 @@ describe('createPageExtension', () => {
|
||||
captureEvent,
|
||||
});
|
||||
|
||||
const extension = createPageExtension({
|
||||
id: 'plugin.page',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>Component</div>,
|
||||
});
|
||||
|
||||
const output = extension.factory({
|
||||
source: createPlugin({ id: 'plugin ' }),
|
||||
config: { path: '/' },
|
||||
inputs: {},
|
||||
});
|
||||
|
||||
renderWithEffects(wrapInTestApp(output.element as unknown as JSX.Element));
|
||||
createExtensionTester(
|
||||
createPageExtension({
|
||||
id: 'plugin.page',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>Component</div>,
|
||||
}),
|
||||
).render();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(captureEvent).toHaveBeenCalledWith(
|
||||
|
||||
@@ -75,7 +75,7 @@ export function createPageExtension<
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
},
|
||||
factory({ config, inputs, source }) {
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ config, inputs })
|
||||
@@ -86,7 +86,7 @@ export function createPageExtension<
|
||||
path: config.path,
|
||||
routeRef: options.routeRef,
|
||||
element: (
|
||||
<ExtensionBoundary id={id} source={source} routable>
|
||||
<ExtensionBoundary node={node} routable>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppNode } from '../apis';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { Expand } from '../types';
|
||||
import { ExtensionDataRef } from './createExtensionDataRef';
|
||||
import { ExtensionInput } from './createExtensionInput';
|
||||
import { BackstagePlugin } from './createPlugin';
|
||||
|
||||
/** @public */
|
||||
export type AnyExtensionDataMap = {
|
||||
@@ -80,7 +80,7 @@ export interface CreateExtensionOptions<
|
||||
output: TOutput;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
@@ -96,7 +96,7 @@ export interface Extension<TConfig> {
|
||||
output: AnyExtensionDataMap;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
|
||||
@@ -113,7 +113,7 @@ export function createEntityCardExtension<
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config, inputs, source }) {
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ inputs })
|
||||
@@ -122,7 +122,7 @@ export function createEntityCardExtension<
|
||||
|
||||
return {
|
||||
element: (
|
||||
<ExtensionBoundary id={id} source={source}>
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
@@ -179,7 +179,7 @@ export function createEntityContentExtension<
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config, inputs, source }) {
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ inputs })
|
||||
@@ -191,7 +191,7 @@ export function createEntityContentExtension<
|
||||
title: config.title,
|
||||
routeRef: options.routeRef,
|
||||
element: (
|
||||
<ExtensionBoundary id={id} source={source} routable>
|
||||
<ExtensionBoundary node={node} routable>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
|
||||
@@ -43,7 +43,7 @@ export function createCatalogFilterExtension<
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory({ config, source }) {
|
||||
factory({ config, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ config })
|
||||
@@ -52,7 +52,7 @@ export function createCatalogFilterExtension<
|
||||
|
||||
return {
|
||||
element: (
|
||||
<ExtensionBoundary id={id} source={source}>
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
|
||||
@@ -107,7 +107,7 @@ export function createSearchResultListItemExtension<
|
||||
output: {
|
||||
item: searchResultItemExtensionData,
|
||||
},
|
||||
factory({ config, source }) {
|
||||
factory({ config, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.component({ config })
|
||||
@@ -118,7 +118,7 @@ export function createSearchResultListItemExtension<
|
||||
item: {
|
||||
predicate: options.predicate,
|
||||
component: props => (
|
||||
<ExtensionBoundary id={id} source={source}>
|
||||
<ExtensionBoundary node={node}>
|
||||
<SearchResultListItemExtension
|
||||
rank={props.rank}
|
||||
result={props.result}
|
||||
|
||||
Reference in New Issue
Block a user