diff --git a/.changeset/nervous-penguins-sort.md b/.changeset/nervous-penguins-sort.md new file mode 100644 index 0000000000..0c3ae4fcbe --- /dev/null +++ b/.changeset/nervous-penguins-sort.md @@ -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. diff --git a/.changeset/stale-walls-cross.md b/.changeset/stale-walls-cross.md new file mode 100644 index 0000000000..79d682631b --- /dev/null +++ b/.changeset/stale-walls-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Updates to provide `node` to extension factories instead of `id` and `source`. diff --git a/.changeset/wicked-elephants-think.md b/.changeset/wicked-elephants-think.md new file mode 100644 index 0000000000..73fc9621bf --- /dev/null +++ b/.changeset/wicked-elephants-think.md @@ -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`. diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 68a95fa1fe..802c71f729 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -15,6 +15,7 @@ */ import { + AppNode, Extension, createExtension, createExtensionDataRef, @@ -52,15 +53,27 @@ const simpleExtension = createExtension({ function makeSpec( extension: Extension, - config?: TConfig, + spec?: Partial, ): AppNodeSpec { return { id: extension.id, attachTo: extension.attachTo, disabled: extension.disabled, extension, - config, source: undefined, + ...spec, + }; +} + +function makeNode( + extension: Extension, + spec?: Partial, +): AppNode { + return { + spec: makeSpec(extension, spec), + edges: { + attachments: new Map(), + }, }; } @@ -71,7 +84,7 @@ function makeInstanceWithId( return { id: extension.id, instance: createAppNodeInstance({ - spec: makeSpec(extension, config), + node: makeNode(extension, { config }), attachments: new Map(), }), }; @@ -80,7 +93,7 @@ function makeInstanceWithId( 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' }, diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 003abbb97e..2b1ab04c25 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -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 = { -readonly [P in keyof T]: T[P]; @@ -99,11 +95,11 @@ function resolveInputs( /** @internal */ export function createAppNodeInstance(options: { - spec: AppNodeSpec; + node: AppNode; attachments: ReadonlyMap; }): 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(); const extensionDataRefs = new Set>(); @@ -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).instance = createAppNodeInstance({ - spec: node.spec, + node, attachments: instantiatedAttachments, }); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index f1537fd5b7..dc3de16065 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -339,7 +339,7 @@ export interface CreateExtensionOptions< disabled?: boolean; // (undocumented) factory(options: { - source?: BackstagePlugin; + node: AppNode; config: TConfig; inputs: Expand>; }): Expand>; @@ -493,7 +493,7 @@ export interface Extension { 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) diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 88bdcc6617..49a9f95ed2 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -34,15 +34,11 @@ const wrapInBoundaryExtension = (element: JSX.Element) => { path: coreExtensionData.routePath, routeRef: coreExtensionData.routeRef.optional(), }, - factory({ source }) { + factory({ node }) { return { routeRef, path: '/', - element: ( - - {element} - - ), + element: {element}, }; }, }); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index a9ea297216..8fad8d8336 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -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 ( - + {children} diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 81843558da..f75821944a 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -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 () =>
Component
, - }); - - 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 () =>
Component
, + }), + ).render(); await waitFor(() => expect(captureEvent).toHaveBeenCalledWith( diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index f5e258a1e4..99733ba367 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -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: ( - + ), diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index ff011a5875..9443caeb66 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -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; factory(options: { - source?: BackstagePlugin; + node: AppNode; config: TConfig; inputs: Expand>; }): Expand>; @@ -96,7 +96,7 @@ export interface Extension { output: AnyExtensionDataMap; configSchema?: PortableSchema; factory(options: { - source?: BackstagePlugin; + node: AppNode; config: TConfig; inputs: Record< string, diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index 8d0436e4be..a339816742 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -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: ( - + ), @@ -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: ( - + ), diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx index cf3dbd318a..7dae30c1fa 100644 --- a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx +++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx @@ -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: ( - + ), diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index 80e464fe4e..bb738a2b08 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -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 => ( - +