diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 77ff65f330..e8b429f440 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -41,13 +41,11 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { // pull in default extension instance from discovered packages // apply config to adjust default extension instances and add more - const extensionParams = mergeExtensionParameters( - [ - ...options.plugins.flatMap(plugin => plugin.extensions), - ...builtinExtensions, - ], - readAppExtensionParameters(appConfig), - ); + const extensionParams = mergeExtensionParameters({ + sources: options.plugins, + builtinExtensions, + parameters: readAppExtensionParameters(appConfig), + }); // TODO: validate the config of all extension instances // We do it at this point to ensure that merging (if any) of config has already happened @@ -96,6 +94,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { return createExtensionInstance({ extension: instanceParams.extension, + source: instanceParams.source, config: instanceParams.config, attachments, }); diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts index 633baf6919..911955f50a 100644 --- a/packages/frontend-app-api/src/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/createExtensionInstance.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Extension } from '@backstage/frontend-plugin-api'; +import { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; /** @internal */ @@ -28,9 +28,10 @@ export interface ExtensionInstance { export function createExtensionInstance(options: { extension: Extension; config: unknown; + source?: BackstagePlugin; attachments: Record; }): ExtensionInstance { - const { extension, config, attachments } = options; + const { extension, config, source, attachments } = options; const extensionData = new Map(); let parsedConfig: unknown; @@ -44,6 +45,7 @@ export function createExtensionInstance(options: { try { extension.factory({ + source, config: parsedConfig, bind: mapValues(extension.output, ref => { return (value: unknown) => extensionData.set(ref.id, value); diff --git a/packages/frontend-app-api/src/wiring/parameters.test.ts b/packages/frontend-app-api/src/wiring/parameters.test.ts index 71948e340e..10b0e2fc6f 100644 --- a/packages/frontend-app-api/src/wiring/parameters.test.ts +++ b/packages/frontend-app-api/src/wiring/parameters.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { Extension } from '@backstage/frontend-plugin-api'; +import { createPlugin, Extension } from '@backstage/frontend-plugin-api'; import { JsonValue } from '@backstage/types'; import { expandShorthandExtensionParameters, @@ -33,15 +33,25 @@ function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') { describe('mergeExtensionParameters', () => { it('should filter out disabled extension instances', () => { - expect(mergeExtensionParameters([makeExt('a', 'disabled')], [])).toEqual( - [], - ); + expect( + mergeExtensionParameters({ + sources: [], + builtinExtensions: [makeExt('a', 'disabled')], + parameters: [], + }), + ).toEqual([]); }); it('should pass through extension instances', () => { const a = makeExt('a'); const b = makeExt('b'); - expect(mergeExtensionParameters([a, b], [])).toEqual([ + expect( + mergeExtensionParameters({ + sources: [], + builtinExtensions: [a, b], + parameters: [], + }), + ).toEqual([ { extension: a, at: 'root' }, { extension: b, at: 'root' }, ]); @@ -50,18 +60,20 @@ describe('mergeExtensionParameters', () => { it('should override attachment points', () => { const a = makeExt('a'); const b = makeExt('b'); + const pluginA = createPlugin({ id: 'test', extensions: [a] }); expect( - mergeExtensionParameters( - [a, b], - [ + mergeExtensionParameters({ + sources: [pluginA], + builtinExtensions: [b], + parameters: [ { id: 'b', at: 'derp', }, ], - ), + }), ).toEqual([ - { extension: a, at: 'root' }, + { extension: a, at: 'root', source: pluginA }, { extension: b, at: 'derp' }, ]); }); @@ -69,10 +81,12 @@ describe('mergeExtensionParameters', () => { it('should fully override configuration and duplicate', () => { const a = makeExt('a'); const b = makeExt('b'); + const plugin = createPlugin({ id: 'test', extensions: [a, b] }); expect( - mergeExtensionParameters( - [a, b], - [ + mergeExtensionParameters({ + sources: [plugin], + builtinExtensions: [], + parameters: [ { id: 'a', config: { foo: { bar: 1 } }, @@ -86,10 +100,10 @@ describe('mergeExtensionParameters', () => { config: { foo: { qux: 3 } }, }, ], - ), + }), ).toEqual([ - { extension: a, at: 'root', config: { foo: { bar: 1 } } }, - { extension: b, at: 'root', config: { foo: { qux: 3 } } }, + { extension: a, at: 'root', source: plugin, config: { foo: { bar: 1 } } }, + { extension: b, at: 'root', source: plugin, config: { foo: { qux: 3 } } }, ]); }); @@ -97,9 +111,10 @@ describe('mergeExtensionParameters', () => { const a = makeExt('a', 'disabled'); const b = makeExt('b', 'disabled'); expect( - mergeExtensionParameters( - [a, b], - [ + mergeExtensionParameters({ + sources: [createPlugin({ id: 'empty', extensions: [] })], + builtinExtensions: [a, b], + parameters: [ { id: 'b', disabled: false, @@ -109,7 +124,7 @@ describe('mergeExtensionParameters', () => { disabled: false, }, ], - ), + }), ).toEqual([ { extension: b, at: 'root' }, { extension: a, at: 'root' }, diff --git a/packages/frontend-app-api/src/wiring/parameters.ts b/packages/frontend-app-api/src/wiring/parameters.ts index 2f72b1cf4a..1b2831aaca 100644 --- a/packages/frontend-app-api/src/wiring/parameters.ts +++ b/packages/frontend-app-api/src/wiring/parameters.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { Extension } from '@backstage/frontend-plugin-api'; +import { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api'; import { JsonValue } from '@backstage/types'; export interface ExtensionParameters { @@ -183,23 +183,41 @@ export function expandShorthandExtensionParameters( export interface ExtensionInstanceParameters { extension: Extension; + source?: BackstagePlugin; at: string; config?: unknown; } /** @internal */ -export function mergeExtensionParameters( - base: Extension[], - parameters: Array, -): ExtensionInstanceParameters[] { - const overrides = base.map(extension => ({ - extension, - params: { - at: extension.at, - disabled: extension.disabled, - config: undefined as unknown, - }, - })); +export function mergeExtensionParameters(options: { + sources: BackstagePlugin[]; + builtinExtensions: Extension[]; + parameters: Array; +}): ExtensionInstanceParameters[] { + const { sources, builtinExtensions, parameters } = options; + + const overrides = [ + ...sources.flatMap(plugin => + plugin.extensions.map(extension => ({ + extension, + params: { + source: plugin, + at: extension.at, + disabled: extension.disabled, + config: undefined as unknown, + }, + })), + ), + ...builtinExtensions.map(extension => ({ + extension, + params: { + source: undefined, + at: extension.at, + disabled: extension.disabled, + config: undefined as unknown, + }, + })), + ]; for (const overrideParam of parameters) { const existingIndex = overrides.findIndex( @@ -234,6 +252,7 @@ export function mergeExtensionParameters( .map(param => ({ extension: param.extension, at: param.params.at, + source: param.params.source, config: param.params.config, })); } diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 92fc11ef17..514ca0d82e 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -9,6 +9,8 @@ import { AnyApiFactory } from '@backstage/core-plugin-api'; import { AnyApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { JsonObject } from '@backstage/types'; +import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; import { z } from 'zod'; import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -95,6 +97,7 @@ export interface CreateExtensionOptions< disabled?: boolean; // (undocumented) factory(options: { + source?: BackstagePlugin; bind: ExtensionDataBind; config: TConfig; inputs: { @@ -166,6 +169,7 @@ export interface Extension { disabled: boolean; // (undocumented) factory(options: { + source?: BackstagePlugin; bind: ExtensionDataBind; config: TConfig; inputs: Record>>; @@ -183,6 +187,19 @@ export interface Extension { output: AnyExtensionDataMap; } +// @public (undocumented) +export function ExtensionBoundary( + props: ExtensionBoundaryProps, +): React_2.JSX.Element; + +// @public (undocumented) +export interface ExtensionBoundaryProps { + // (undocumented) + children: ReactNode; + // (undocumented) + source?: BackstagePlugin; +} + // @public (undocumented) export type ExtensionDataBind = { [K in keyof TData]: (value: TData[K]['T']) => void; diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx new file mode 100644 index 0000000000..c6a218c15f --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -0,0 +1,29 @@ +/* + * 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. + */ + +import React, { ReactNode } from 'react'; +import { BackstagePlugin } from '../wiring'; + +/** @public */ +export interface ExtensionBoundaryProps { + children: ReactNode; + source?: BackstagePlugin; +} + +/** @public */ +export function ExtensionBoundary(props: ExtensionBoundaryProps) { + return <>{props.children}; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts new file mode 100644 index 0000000000..7056a59271 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { + ExtensionBoundary, + type ExtensionBoundaryProps, +} from './ExtensionBoundary'; diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 22b3df318d..b3f8942d0d 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; +import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod'; import { AnyExtensionDataMap, @@ -72,7 +73,7 @@ export function createPageExtension< }, inputs: options.inputs, configSchema, - factory({ bind, config, inputs }) { + factory({ bind, config, inputs, source }) { const LazyComponent = React.lazy(() => options .component({ config, inputs }) @@ -80,9 +81,11 @@ export function createPageExtension< ); bind.path(config.path); bind.component(() => ( - - - + + + + + )); }, }); diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index a9c4a4f2e7..8c87986a14 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -24,6 +24,7 @@ export { createSchemaFromZod, type PortableSchema, } from './createSchemaFromZod'; +export * from './components'; export * from './extensions'; export { coreExtensionData, diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts index 2c9f515d9d..8c59deabfb 100644 --- a/packages/frontend-plugin-api/src/types.ts +++ b/packages/frontend-plugin-api/src/types.ts @@ -17,6 +17,7 @@ import { AnyApiFactory } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { PortableSchema } from './createSchemaFromZod'; +import { BackstagePlugin } from './wiring'; /** @public */ export type ExtensionDataRef = { @@ -64,6 +65,7 @@ export interface CreateExtensionOptions< output: TData; configSchema?: PortableSchema; factory(options: { + source?: BackstagePlugin; bind: ExtensionDataBind; config: TConfig; inputs: { @@ -84,6 +86,7 @@ export interface Extension { output: AnyExtensionDataMap; configSchema?: PortableSchema; factory(options: { + source?: BackstagePlugin; bind: ExtensionDataBind; config: TConfig; inputs: Record>>;