chore: make more things swoppable

Signed-off-by: benjdlambert <ben@blam.sh>

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-08-06 14:24:17 +02:00
parent d996f05ae5
commit 4b3ba2b048
17 changed files with 228 additions and 172 deletions
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ComponentRef } from '../../components';
import { SwappableComponentRef } from '../../components';
import { createApiRef } from '@backstage/core-plugin-api';
/**
@@ -22,12 +22,12 @@ import { createApiRef } from '@backstage/core-plugin-api';
*
* @public
*/
export interface ComponentsApi {
getComponent<
export interface SwappableComponentsApi {
getComponentLoader<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
>(
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>,
ref: SwappableComponentRef<TInnerComponentProps, TExternalComponentProps>,
):
| (() => (props: TInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>)
@@ -35,10 +35,10 @@ export interface ComponentsApi {
}
/**
* The `ApiRef` of {@link ComponentsApi}.
* The `ApiRef` of {@link SwappableComponentsApi}.
*
* @public
*/
export const componentsApiRef = createApiRef<ComponentsApi>({
id: 'core.components',
export const swappableComponentsApiRef = createApiRef<SwappableComponentsApi>({
id: 'core.swappableComponents',
});
@@ -34,7 +34,7 @@ export * from './auth';
export * from './AlertApi';
export * from './AppThemeApi';
export * from './ComponentsApi';
export * from './SwappableComponentsApi';
export * from './ConfigApi';
export * from './DiscoveryApi';
export * from './ErrorApi';
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentRef } from '../components';
import { SwappableComponentRef } from '../components';
import {
createExtensionBlueprint,
createExtensionBlueprintParams,
@@ -21,14 +21,14 @@ import {
} from '../wiring';
export const componentDataRef = createExtensionDataRef<{
ref: ComponentRef;
ref: SwappableComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
}>().with({ id: 'core.component.component' });
/**
* Blueprint for creating swappable components from a componentRef and a loader
* Blueprint for creating swappable components from a SwappableComponentRef and a loader
*
* @public
*/
@@ -39,11 +39,14 @@ export const SwappableComponentBlueprint = createExtensionBlueprint({
dataRefs: {
component: componentDataRef,
},
defineParams<Ref extends ComponentRef<any>>(params: {
component: Ref extends ComponentRef<any, infer IExternalComponentProps>
? { ref: Ref } & ((props: IExternalComponentProps) => JSX.Element)
defineParams<Ref extends SwappableComponentRef<any>>(params: {
component: Ref extends SwappableComponentRef<
any,
infer IExternalComponentProps
>
? { ref: Ref } & ((props: IExternalComponentProps) => JSX.Element | null)
: never;
loader: Ref extends ComponentRef<infer IInnerComponentProps, any>
loader: Ref extends SwappableComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
@@ -14,19 +14,19 @@
* limitations under the License.
*/
import { OpaqueComponentRef } from '@internal/frontend';
import { componentsApiRef, useApi } from '../apis';
import { OpaqueSwappableComponentRef } from '@internal/frontend';
import { swappableComponentsApiRef, useApi } from '../apis';
import { lazy, Suspense } from 'react';
/** @public */
export type ComponentRef<
export type SwappableComponentRef<
TInnerComponentProps extends {} = {},
TExternalComponentProps extends {} = TInnerComponentProps,
> = {
id: string;
TProps: TInnerComponentProps;
TExternalProps: TExternalComponentProps;
$$type: '@backstage/ComponentRef';
$$type: '@backstage/SwappableComponentRef';
};
/**
@@ -47,7 +47,7 @@ export type CreateSwappableComponentOptions<
const useComponentRefApi = () => {
try {
return useApi(componentsApiRef);
return useApi(swappableComponentsApiRef);
} catch (e) {
return undefined;
}
@@ -59,9 +59,9 @@ function makeComponentFromRef<
>({
ref,
}: {
ref: ComponentRef<InternalComponentProps, ExternalComponentProps>;
ref: SwappableComponentRef<InternalComponentProps, ExternalComponentProps>;
}): (props: ExternalComponentProps) => JSX.Element {
const internalRef = OpaqueComponentRef.toInternal(ref);
const internalRef = OpaqueSwappableComponentRef.toInternal(ref);
const FallbackComponent = (p: JSX.IntrinsicAttributes) => (
<div data-testid={ref.id} {...p} />
);
@@ -69,7 +69,7 @@ function makeComponentFromRef<
const ComponentRefImpl = (props: ExternalComponentProps) => {
const api = useComponentRefApi();
const ComponentOrPromise =
api?.getComponent<any>(ref)?.() ??
api?.getComponentLoader<any>(ref)?.() ??
internalRef.loader?.() ??
FallbackComponent;
@@ -108,25 +108,26 @@ export function createSwappableComponent<
TInnerComponentProps,
TExternalComponentProps
>,
): ((props: TExternalComponentProps) => JSX.Element) & {
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>;
): ((props: TExternalComponentProps) => JSX.Element | null) & {
ref: SwappableComponentRef<TInnerComponentProps, TExternalComponentProps>;
} {
const ref = OpaqueComponentRef.createInstance('v1', {
const ref = OpaqueSwappableComponentRef.createInstance('v1', {
id: options.id,
TProps: null as unknown as TInnerComponentProps,
TExternalProps: null as unknown as TExternalComponentProps,
toString() {
return `ComponentRef{id=${options.id}}`;
return `SwappableComponentRef{id=${options.id}}`;
},
loader: options.loader as (typeof OpaqueComponentRef.TInternal)['loader'],
loader:
options.loader as (typeof OpaqueSwappableComponentRef.TInternal)['loader'],
transformProps:
options.transformProps as (typeof OpaqueComponentRef.TInternal)['transformProps'],
options.transformProps as (typeof OpaqueSwappableComponentRef.TInternal)['transformProps'],
});
const component = makeComponentFromRef({ ref });
Object.assign(component, { ref });
return component as {
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>;
} & ((props: object) => JSX.Element);
ref: SwappableComponentRef<TInnerComponentProps, TExternalComponentProps>;
} & ((props: object) => JSX.Element | null);
}
@@ -21,7 +21,7 @@ export {
export {
createSwappableComponent,
type CreateSwappableComponentOptions,
type ComponentRef,
type SwappableComponentRef,
} from './createSwappableComponent';
export { useAppNode } from './AppNodeProvider';
export * from './DefaultSwappableComponents';