diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx index 7d06d55a92..b2ab5d5aef 100644 --- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx @@ -18,9 +18,9 @@ import { createComponentRef } from '@backstage/frontend-plugin-api'; import { DefaultComponentsApi } from './DefaultComponentsApi'; import { render, screen } from '@testing-library/react'; -const testRefA = createComponentRef({ id: 'test.a' }); -const testRefB1 = createComponentRef({ id: 'test.b' }); -const testRefB2 = createComponentRef({ id: 'test.b' }); +const testRefA = createComponentRef({ id: 'test.a', mode: 'sync' }); +const testRefB1 = createComponentRef({ id: 'test.b', mode: 'sync' }); +const testRefB2 = createComponentRef({ id: 'test.b', mode: 'sync' }); describe('DefaultComponentsApi', () => { it('should provide components', () => { diff --git a/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.test.tsx new file mode 100644 index 0000000000..b0095e4350 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 { createComponentRef } from '../components'; +import { ComponentImplementationBlueprint } from './ComponentImplementationBlueprint'; + +describe('ComponentImplementationBlueprint', () => { + it('should allow defining a component override for sync component ref', () => { + const componentRef = createComponentRef({ + id: 'test.component', + mode: 'sync', + defaultComponent: (props: { hello: string }) =>
{props.hello}
, + }); + + const extension = ComponentImplementationBlueprint.make({ + params: define => + define({ + ref: componentRef, + component: props => { + // @ts-expect-error + const t: number = props.hello; + + return
Override {props.hello}
; + }, + }), + }); + + expect(extension).toBeDefined(); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.ts new file mode 100644 index 0000000000..d903d63f5e --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2025 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 { ComponentRef } from '../components'; +import { + createExtensionBlueprint, + createExtensionBlueprintParams, + createExtensionDataRef, +} from '../wiring'; + +export const componentDataRef = createExtensionDataRef<{ + ref: ComponentRef; + component: + | ((props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + type: 'sync' | 'async'; +}>().with({ id: 'core.component.component' }); + +export const ComponentImplementationBlueprint = createExtensionBlueprint({ + kind: 'component', + attachTo: { id: 'api:app/components', input: 'components' }, + output: [componentDataRef], + dataRefs: { + component: componentDataRef, + }, + defineParams>(params: { + ref: Ref; + component: Ref extends ComponentRef< + infer IInnerComponentProps, + any, + infer IMode + > + ? IMode extends 'sync' + ? (props: IInnerComponentProps) => JSX.Element + : IMode extends 'async' + ? () => Promise<(props: IInnerComponentProps) => JSX.Element> + : never + : never; + }) { + return createExtensionBlueprintParams(params); + }, + *factory(params) { + yield componentDataRef({ + ref: params.ref, + component: params.component, + type: params.ref.mode, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/components/createComponentRef.tsx b/packages/frontend-plugin-api/src/components/createComponentRef.tsx index ebf707fcca..f0a1e36371 100644 --- a/packages/frontend-plugin-api/src/components/createComponentRef.tsx +++ b/packages/frontend-plugin-api/src/components/createComponentRef.tsx @@ -20,27 +20,27 @@ import { OpaqueComponentRef } from '@internal/frontend'; export type ComponentRef< TInnerComponentProps extends {} = {}, TExternalComponentProps extends {} = TInnerComponentProps, + TMode extends 'sync' | 'async' = 'sync' | 'async', > = { id: string; TProps: TInnerComponentProps; TExternalProps: TExternalComponentProps; + TMode: TMode; $$type: '@backstage/ComponentRef'; }; -export interface ComponentRefOptions< +export type ComponentRefOptions< TInnerComponentProps extends {}, - TExternalComponentProps extends {}, - TMode extends 'sync' | 'async', -> { + TExternalComponentProps extends {} = TInnerComponentProps, +> = { id: string; - mode: TMode; - defaultComponent?: TMode extends 'async' - ? () => Promise<(props: TInnerComponentProps) => JSX.Element> + componentAsync: TMode extends 'async' + ? () => Promise<(props: TInnerComponentProps) => JSX.Element | null> : TMode extends 'sync' - ? (props: TInnerComponentProps) => JSX.Element + ? (props: TInnerComponentProps) => JSX.Element | null : never; transformProps?: (props: TExternalComponentProps) => TInnerComponentProps; -} +}; /** * Creates a new component ref that is synchronous. @@ -55,7 +55,7 @@ export function createComponentRef< TExternalComponentProps, 'sync' >, -): ComponentRef; +): ComponentRef; /** * Creates a new component ref that is asynchronous. @@ -70,22 +70,24 @@ export function createComponentRef< TExternalComponentProps, 'async' >, -): ComponentRef; +): ComponentRef; export function createComponentRef< TInnerComponentProps extends {}, - TExternalComponentProps extends {}, + TExternalComponentProps extends {} = TInnerComponentProps, + TMode extends 'sync' | 'async' = 'sync' | 'async', >( options: ComponentRefOptions< TInnerComponentProps, TExternalComponentProps, - 'async' | 'sync' + TMode >, ): ComponentRef { return OpaqueComponentRef.createInstance('v1', { id: options.id, TProps: null as unknown as TInnerComponentProps, TExternalProps: null as unknown as TExternalComponentProps, + TMode: null as unknown as TMode, toString() { return `ComponentRef{id=${options.id}}`; }, diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index f94551755f..fd9fb3b9f5 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -214,7 +214,6 @@ type AnyParamsInput = * @public */ export interface ExtensionBlueprint< - // TParamsMapper extends (params: any) => object, T extends ExtensionBlueprintParameters = ExtensionBlueprintParameters, > { dataRefs: T['dataRefs'];