From f749bb8fa989e5f4c29608a903cc9119cd745390 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 25 Jun 2024 14:25:22 +0200 Subject: [PATCH] chore: extension overrides spike Signed-off-by: blam --- .../core-compat-api/src/convertLegacyApp.ts | 72 ++++++++++- .../src/wiring/createExtension.test.ts | 33 +++++ .../src/wiring/createExtension.ts | 121 ++++++++++++++---- .../src/wiring/createExtensionKind.ts | 4 +- 4 files changed, 194 insertions(+), 36 deletions(-) diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 72ce4b5e61..567e5efcad 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -26,6 +26,7 @@ import { coreExtensionData, createExtension, createExtensionInput, + createExtensionKind, createExtensionOverrides, } from '@backstage/frontend-plugin-api'; import { getComponentData } from '@backstage/core-plugin-api'; @@ -128,21 +129,80 @@ export function convertLegacyApp( }; }, }); - const CoreNavOverride = createExtension({ - namespace: 'app', - name: 'nav', - attachTo: { id: 'app/layout', input: 'nav' }, - output: {}, + + const CoreNavOverride = CurrentCoreNav.override({ + // namespace: 'app', + // name: 'nav', + // attachTo: { id: 'app/layout', input: 'nav' }, + // output: {}, factory: () => ({}), disabled: true, }); + createExtensionOverride({ + extension: CurrentCoreNav, + factory: () => null, + }); + const collectedRoutes = collectLegacyRoutes(routesEl); return [ ...collectedRoutes, createExtensionOverrides({ - extensions: [CoreLayoutOverride, CoreNavOverride], + extensions: [CoreNavOverride, CoreNavOverride], }), ]; } + +const EntityCardExtension = createExtensionKind({ + kind: 'entity-card', + attachTo: { id: 'entity-card', input: 'default' }, + inputs: { + loader: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }, props: { title: string }) { + console.log(inputs.loader); + return { + element: React.createElement('h1'), + }; + }, +}); + +const GithubCard = EntityCardExtension.new({ + props: { + title: 'GitHub Card', + }, + factory({ inputs: { loader } }) { + console.log(loader); + return { + element: React.createElement('h2'), + }; + }, +}); + +GithubCard.override({ + attachTo: { id: 'entity-card', input: 'github' }, + inputs: { + loader: createExtensionInput( + { + element: coreExtensionData.reactElement, + }, + { singleton: false }, + ), + loader2: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + factory({ originalFactory, inputs }) { + inputs.loader2; + inputs.loader; + return originalFactory({ + inputsOverride: inputs, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 42ab21db80..cfef69b0fe 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -291,4 +291,37 @@ describe('createExtension', () => { 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); }); + + describe('override', () => { + it('should create an extension override', () => { + const extension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: { + foo: stringData, + }, + factory() { + return { + foo: 'bar', + }; + }, + }); + + const override = extension.override({ + attachTo: { + id: 'root', + input: 'default2', + }, + factory() { + return { + foo: 'baz', + }; + }, + }); + + expect(String(override)).toBe( + 'ExtensionDefinition{namespace=test,attachTo=root@default2}', + ); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 28dc6a4e29..b56cc39fc9 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -99,7 +99,33 @@ export interface CreateExtensionOptions< } /** @public */ -export interface ExtensionDefinition { +export interface ExtensionDefinitionOverrides< + TOriginalConfig, + TOriginalInputs extends AnyExtensionInputMap, + TOverrideInputs extends AnyExtensionInputMap, +> { + readonly disabled?: boolean; + readonly attachTo?: { id: string; input: string }; + // TODO: inputs (only adding to the list? or redefine arity and superset of old type on existing input?) + // TODO: config (any merging needed on there?) + inputs?: TOverrideInputs; + factory?(options: { + node: AppNode; + config: TOriginalConfig; + inputs: Expand>; + originalFactory: (originalFactoryOptions?: { + inputsOverride?: ResolvedExtensionInputs< + TOriginalInputs & TOverrideInputs + >; + }) => ExtensionDataValues; + }): ExtensionDataValues; +} + +/** @public */ +export interface ExtensionDefinition< + TConfig, + TInputs extends AnyExtensionInputMap, +> { $$type: '@backstage/ExtensionDefinition'; readonly kind?: string; readonly namespace?: string; @@ -107,13 +133,18 @@ export interface ExtensionDefinition { readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema; + override( + overrides: ExtensionDefinitionOverrides, + ): ExtensionDefinition; } /** @internal */ -export interface InternalExtensionDefinition - extends ExtensionDefinition { +export interface InternalExtensionDefinition< + TConfig, + TInputs extends AnyExtensionInputMap, +> extends ExtensionDefinition { readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; + readonly inputs: TInputs; readonly output: AnyExtensionDataMap; factory(context: { node: AppNode; @@ -123,10 +154,13 @@ export interface InternalExtensionDefinition } /** @internal */ -export function toInternalExtensionDefinition( - overrides: ExtensionDefinition, -): InternalExtensionDefinition { - const internal = overrides as InternalExtensionDefinition; +export function toInternalExtensionDefinition< + TConfig, + TInputs extends AnyExtensionInputMap, +>( + overrides: ExtensionDefinition, +): InternalExtensionDefinition { + const internal = overrides as InternalExtensionDefinition; if (internal.$$type !== '@backstage/ExtensionDefinition') { throw new Error( `Invalid extension definition instance, bad type '${internal.$$type}'`, @@ -147,38 +181,69 @@ export function createExtension< TConfig = never, >( options: CreateExtensionOptions, -): ExtensionDefinition { +): ExtensionDefinition { + const { + kind, + namespace, + name, + attachTo, + disabled = false, + inputs = {} as TInputs, + output, + configSchema, + factory, + } = options; + return { $$type: '@backstage/ExtensionDefinition', version: 'v1', - kind: options.kind, - namespace: options.namespace, - name: options.name, - attachTo: options.attachTo, - disabled: options.disabled ?? false, - inputs: options.inputs ?? {}, - output: options.output, - configSchema: options.configSchema, - factory({ inputs, ...rest }) { + kind, + namespace, + name, + attachTo, + disabled, + inputs, + output, + configSchema, + factory({ inputs: factoryInputs, ...rest }) { // TODO: Simplify this, but TS wouldn't infer the input type for some reason - return options.factory({ - inputs: inputs as Expand>, + return factory({ + inputs: factoryInputs as Expand>, ...rest, }); }, toString() { const parts: string[] = []; - if (options.kind) { - parts.push(`kind=${options.kind}`); + if (kind) { + parts.push(`kind=${kind}`); } - if (options.namespace) { - parts.push(`namespace=${options.namespace}`); + if (namespace) { + parts.push(`namespace=${namespace}`); } - if (options.name) { - parts.push(`name=${options.name}`); + if (name) { + parts.push(`name=${name}`); } - parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); + parts.push(`attachTo=${attachTo.id}@${attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - } as InternalExtensionDefinition; + override( + overrides: ExtensionDefinitionOverrides< + TConfig, + TInputs, + TOverrideInputs + >, + ): ExtensionDefinition { + return createExtension({ + kind, + namespace, + name, + attachTo: overrides.attachTo ?? attachTo, + disabled: overrides.disabled ?? disabled, + inputs: { ...inputs, ...overrides.inputs }, + output, + configSchema, + factory, + }); + }, + } as InternalExtensionDefinition; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts index 52bf711822..353fb8ab70 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { AppNode } from '../apis'; import { PortableSchema } from '../schema'; import { Expand } from '../types'; @@ -21,6 +20,7 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, ExtensionDataValues, + ExtensionDefinition, ResolvedExtensionInputs, createExtension, } from './createExtension'; @@ -107,7 +107,7 @@ export class ExtensionKind< }, options: TOptions, ): Expand>; - }) { + }): ExtensionDefinition { return createExtension({ kind: this.options.kind, namespace: args.namespace ?? this.options.namespace,