chore: adaptable -> swappable

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

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-08-06 13:36:21 +02:00
parent 3a34fb4cc7
commit d996f05ae5
17 changed files with 156 additions and 155 deletions
@@ -467,6 +467,7 @@ Superfences
superset
supertype
SVGs
Swappable
talkdesk
Talkdesk
Tanzu
@@ -17,10 +17,6 @@ These are the [extension blueprints](../architecture/23-extension-blueprints.md)
An API extension is used to add or override [Utility API factories](../utility-apis/01-index.md) in the app. They are commonly used by plugins for both internal and shared APIs. There are also many built-in Api extensions provided by the framework that you are able to override.
### AdaptableComponent - [Reference](../../reference/frontend-plugin-api.adaptablecomponentblueprint.md)
Adaptable Components are extensions that are used to override implementation of components in the app and plugins.
### NavItem - [Reference](../../reference/frontend-plugin-api.navitemblueprint.md)
Navigation item extensions are used to provide menu items that link to different parts of the app. By default nav items are attached to the app nav extension, which by default is rendered as the left sidebar in the app.
@@ -33,6 +29,10 @@ Page extensions provide content for a particular route in the app. By default pa
Sign-in page extension have a single purpose - to implement a custom sign-in page. They are always attached to the app root extension and are rendered before the rest of the app until the user is signed in.
### SwappableComponent - [Reference](../../reference/frontend-plugin-api.swappablecomponentblueprint.md)
Swappable Components are extensions that are used to replace the implementations of components in the app and plugins.
### Theme - [Reference](../../reference/frontend-plugin-api.themeblueprint.md)
Theme extensions provide custom themes for the app. They are always attached to the app extension and you can have any number of themes extensions installed in an app at once, letting the user choose which theme to use.
@@ -15,14 +15,14 @@
*/
import {
AdaptableComponentBlueprint,
SwappableComponentBlueprint,
NotFoundErrorPage,
} from '@backstage/frontend-plugin-api';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';
import { Button } from '@backstage/core-components';
export function CustomNotFoundErrorPage() {
function CustomNotFoundErrorPage() {
return (
<Box
component="article"
@@ -50,7 +50,7 @@ export function CustomNotFoundErrorPage() {
);
}
export default AdaptableComponentBlueprint.make({
export default SwappableComponentBlueprint.make({
name: 'not-found-error-page',
params: define =>
define({
@@ -16,7 +16,7 @@
import { ComponentType } from 'react';
import {
AdaptableComponentBlueprint,
SwappableComponentBlueprint,
ApiBlueprint,
CoreErrorBoundaryFallbackProps,
createExtension,
@@ -27,9 +27,9 @@ import {
RouterBlueprint,
SignInPageBlueprint,
ThemeBlueprint,
ErrorBoundary as AdaptableErrorBoundary,
NotFoundErrorPage as AdaptableNotFoundErrorPage,
Progress as AdaptableProgress,
ErrorBoundary as SwappableErrorBoundary,
NotFoundErrorPage as SwappableNotFoundErrorPage,
Progress as SwappableProgress,
} from '@backstage/frontend-plugin-api';
import {
AnyApiFactory,
@@ -156,10 +156,10 @@ export function convertLegacyAppOptions(
}
if (Progress) {
extensions.push(
AdaptableComponentBlueprint.make({
SwappableComponentBlueprint.make({
params: define =>
define({
component: AdaptableProgress,
component: SwappableProgress,
loader: () => componentCompatWrapper(Progress),
}),
}),
@@ -168,10 +168,10 @@ export function convertLegacyAppOptions(
if (NotFoundErrorPage) {
extensions.push(
AdaptableComponentBlueprint.make({
SwappableComponentBlueprint.make({
params: define =>
define({
component: AdaptableNotFoundErrorPage,
component: SwappableNotFoundErrorPage,
loader: () => componentCompatWrapper(NotFoundErrorPage),
}),
}),
@@ -190,10 +190,10 @@ export function convertLegacyAppOptions(
);
extensions.push(
AdaptableComponentBlueprint.make({
SwappableComponentBlueprint.make({
params: define =>
define({
component: AdaptableErrorBoundary,
component: SwappableErrorBoundary,
loader: () =>
componentCompatWrapper(WrappedErrorBoundaryFallback),
}),
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { createAdaptableComponent } from '@backstage/frontend-plugin-api';
import { createSwappableComponent } from '@backstage/frontend-plugin-api';
import { DefaultComponentsApi } from './DefaultComponentsApi';
import { render, screen } from '@testing-library/react';
const { ref: testRefA } = createAdaptableComponent({ id: 'test.a' });
const { ref: testRefB1 } = createAdaptableComponent({ id: 'test.b' });
const { ref: testRefB2 } = createAdaptableComponent({ id: 'test.b' });
const { ref: testRefA } = createSwappableComponent({ id: 'test.a' });
const { ref: testRefB1 } = createSwappableComponent({ id: 'test.b' });
const { ref: testRefB2 } = createSwappableComponent({ id: 'test.b' });
describe('DefaultComponentsApi', () => {
it('should provide components', () => {
@@ -17,7 +17,7 @@
import {
ComponentRef,
ComponentsApi,
AdaptableComponentBlueprint,
SwappableComponentBlueprint,
} from '@backstage/frontend-plugin-api';
/**
@@ -34,7 +34,7 @@ export class DefaultComponentsApi implements ComponentsApi {
>;
static fromComponents(
components: Array<typeof AdaptableComponentBlueprint.dataRefs.component.T>,
components: Array<typeof SwappableComponentBlueprint.dataRefs.component.T>,
) {
return new DefaultComponentsApi(
new Map(components.map(entry => [entry.ref.id, entry.loader])),
+78 -78
View File
@@ -94,59 +94,6 @@ import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api';
import { withApis } from '@backstage/core-plugin-api';
import { z } from 'zod';
// @public
export const AdaptableComponentBlueprint: ExtensionBlueprint<{
kind: 'component';
params: <Ref extends ComponentRef<any>>(params: {
component: Ref extends ComponentRef<any, infer IExternalComponentProps>
? {
ref: Ref;
} & ((props: IExternalComponentProps) => JSX.Element)
: never;
loader: Ref extends ComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}) => ExtensionBlueprintParams<{
component: Ref extends ComponentRef<any, infer IExternalComponentProps>
? {
ref: Ref;
} & ((props: IExternalComponentProps) => JSX.Element)
: never;
loader: Ref extends ComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}>;
output: ExtensionDataRef<
{
ref: ComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
},
'core.component.component',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef<
{
ref: ComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
},
'core.component.component',
{}
>;
};
}>;
export { AlertApi };
export { alertApiRef };
@@ -485,31 +432,6 @@ export type CoreNotFoundErrorPageProps = {
// @public (undocumented)
export type CoreProgressProps = {};
// @public
export function createAdaptableComponent<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
>(
options: CreateAdaptableComponentOptions<
TInnerComponentProps,
TExternalComponentProps
>,
): ((props: TExternalComponentProps) => JSX.Element) & {
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>;
};
// @public
export type CreateAdaptableComponentOptions<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
> = {
id: string;
loader?:
| (() => (props: TInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>);
transformProps?: (props: TExternalComponentProps) => TInnerComponentProps;
};
export { createApiFactory };
export { createApiRef };
@@ -901,6 +823,31 @@ export function createSubRouteRef<
parent: RouteRef<ParentParams>;
}): MakeSubRouteRef<PathParams<Path>, ParentParams>;
// @public
export function createSwappableComponent<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
>(
options: CreateSwappableComponentOptions<
TInnerComponentProps,
TExternalComponentProps
>,
): ((props: TExternalComponentProps) => JSX.Element) & {
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>;
};
// @public
export type CreateSwappableComponentOptions<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
> = {
id: string;
loader?:
| (() => (props: TInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>);
transformProps?: (props: TExternalComponentProps) => TInnerComponentProps;
};
export { createTranslationMessages };
export { createTranslationRef };
@@ -1904,6 +1851,59 @@ export interface SubRouteRef<
readonly T: TParams;
}
// @public
export const SwappableComponentBlueprint: ExtensionBlueprint<{
kind: 'component';
params: <Ref extends ComponentRef<any>>(params: {
component: Ref extends ComponentRef<any, infer IExternalComponentProps>
? {
ref: Ref;
} & ((props: IExternalComponentProps) => JSX.Element)
: never;
loader: Ref extends ComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}) => ExtensionBlueprintParams<{
component: Ref extends ComponentRef<any, infer IExternalComponentProps>
? {
ref: Ref;
} & ((props: IExternalComponentProps) => JSX.Element)
: never;
loader: Ref extends ComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}>;
output: ExtensionDataRef<
{
ref: ComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
},
'core.component.component',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef<
{
ref: ComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
},
'core.component.component',
{}
>;
};
}>;
// @public
export const ThemeBlueprint: ExtensionBlueprint<{
kind: 'theme';
@@ -14,19 +14,19 @@
* limitations under the License.
*/
import { renderInTestApp } from '@backstage/frontend-test-utils';
import { createAdaptableComponent } from '../components';
import { AdaptableComponentBlueprint } from './AdaptableComponentBlueprint';
import { createSwappableComponent } from '../components';
import { SwappableComponentBlueprint } from './SwappableComponentBlueprint';
import { PageBlueprint } from './PageBlueprint';
import { waitFor, screen } from '@testing-library/react';
describe('AdaptableComponentBlueprint', () => {
describe('SwappableComponentBlueprint', () => {
it('should allow defining a component override for a component ref', () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'test.component',
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
});
const extension = AdaptableComponentBlueprint.make({
const extension = SwappableComponentBlueprint.make({
params: define =>
define({
component: Component,
@@ -43,7 +43,7 @@ describe('AdaptableComponentBlueprint', () => {
});
it('should render default component refs in the app', async () => {
const TestComponent = createAdaptableComponent({
const TestComponent = createSwappableComponent({
id: 'test.component',
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
});
@@ -66,7 +66,7 @@ describe('AdaptableComponentBlueprint', () => {
});
it('should render a component ref without a default implementation', async () => {
const TestComponent = createAdaptableComponent({
const TestComponent = createSwappableComponent({
id: 'test.component',
});
@@ -89,7 +89,7 @@ describe('AdaptableComponentBlueprint', () => {
});
it('should render a component ref with an async loader implementation', async () => {
const TestComponent = createAdaptableComponent({
const TestComponent = createSwappableComponent({
id: 'test.component',
loader: async () => (props: { hello: string }) =>
<div>{props.hello}</div>,
@@ -113,12 +113,12 @@ describe('AdaptableComponentBlueprint', () => {
});
it('should allow overriding a component ref with the blueprint', async () => {
const TestComponent = createAdaptableComponent({
const TestComponent = createSwappableComponent({
id: 'test.component',
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
});
const extension = AdaptableComponentBlueprint.make({
const extension = SwappableComponentBlueprint.make({
params: define =>
define({
component: TestComponent,
@@ -28,11 +28,11 @@ export const componentDataRef = createExtensionDataRef<{
}>().with({ id: 'core.component.component' });
/**
* Blueprint for creating adaptable components from a componentRef and a loader
* Blueprint for creating swappable components from a componentRef and a loader
*
* @public
*/
export const AdaptableComponentBlueprint = createExtensionBlueprint({
export const SwappableComponentBlueprint = createExtensionBlueprint({
kind: 'component',
attachTo: { id: 'api:app/components', input: 'components' },
output: [componentDataRef],
@@ -33,4 +33,4 @@ export { RouterBlueprint } from './RouterBlueprint';
export { SignInPageBlueprint } from './SignInPageBlueprint';
export { ThemeBlueprint } from './ThemeBlueprint';
export { TranslationBlueprint } from './TranslationBlueprint';
export { AdaptableComponentBlueprint } from './AdaptableComponentBlueprint';
export { SwappableComponentBlueprint } from './SwappableComponentBlueprint';
@@ -19,12 +19,12 @@ import {
CoreNotFoundErrorPageProps,
CoreProgressProps,
} from '../types';
import { createAdaptableComponent } from './createAdaptableComponent';
import { createSwappableComponent } from './createSwappableComponent';
/**
* @public
*/
export const Progress = createAdaptableComponent<CoreProgressProps>({
export const Progress = createSwappableComponent<CoreProgressProps>({
id: 'core.components.progress',
});
@@ -32,7 +32,7 @@ export const Progress = createAdaptableComponent<CoreProgressProps>({
* @public
*/
export const NotFoundErrorPage =
createAdaptableComponent<CoreNotFoundErrorPageProps>({
createSwappableComponent<CoreNotFoundErrorPageProps>({
id: 'core.components.notFoundErrorPage',
});
@@ -40,6 +40,6 @@ export const NotFoundErrorPage =
* @public
*/
export const ErrorBoundary =
createAdaptableComponent<CoreErrorBoundaryFallbackProps>({
createSwappableComponent<CoreErrorBoundaryFallbackProps>({
id: 'core.components.errorBoundary',
});
@@ -29,7 +29,7 @@ import { AppNode } from '../apis';
import { Progress } from '@backstage/core-components';
import { coreExtensionData } from '../wiring';
import { AppNodeProvider } from './AppNodeProvider';
import { ErrorBoundary as ErrorBoundaryComponent } from './DefaultAdaptableComponents';
import { ErrorBoundary as ErrorBoundaryComponent } from './DefaultSwappableComponents';
type RouteTrackerProps = PropsWithChildren<{
enabled?: boolean;
@@ -15,11 +15,11 @@
*/
import { render, screen } from '@testing-library/react';
import { createAdaptableComponent } from './createAdaptableComponent';
import { createSwappableComponent } from './createSwappableComponent';
describe('createAdaptableComponent', () => {
describe('createSwappableComponent', () => {
it('can be created and read', () => {
const { ref } = createAdaptableComponent({ id: 'foo' });
const { ref } = createSwappableComponent({ id: 'foo' });
expect(ref.id).toBe('foo');
expect(String(ref)).toBe('ComponentRef{id=foo}');
});
@@ -27,7 +27,7 @@ describe('createAdaptableComponent', () => {
it('should allow defining a default component implementation', () => {
const Test = () => <div>test</div>;
createAdaptableComponent<{ foo: string }, { bar: string }>({
createSwappableComponent<{ foo: string }, { bar: string }>({
id: 'foo',
loader:
() =>
@@ -35,7 +35,7 @@ describe('createAdaptableComponent', () => {
<Test key={foo} />,
});
createAdaptableComponent<{ foo: string }, { bar: string }>({
createSwappableComponent<{ foo: string }, { bar: string }>({
id: 'foo',
loader:
async () =>
@@ -43,7 +43,7 @@ describe('createAdaptableComponent', () => {
<Test key={foo} />,
});
createAdaptableComponent<{ foo: string }, { bar: string }>({
createSwappableComponent<{ foo: string }, { bar: string }>({
id: 'foo',
});
@@ -51,12 +51,12 @@ describe('createAdaptableComponent', () => {
});
it('should allow transformings props', () => {
createAdaptableComponent<{ foo: string }, { bar: string }>({
createSwappableComponent<{ foo: string }, { bar: string }>({
id: 'foo',
transformProps: props => ({ foo: props.bar }),
});
createAdaptableComponent<{ foo: string }, { bar: string }>({
createSwappableComponent<{ foo: string }, { bar: string }>({
id: 'foo',
// @ts-expect-error - this should be an error as foo is not a string
transformProps: props => ({ foo: 1 }),
@@ -67,7 +67,7 @@ describe('createAdaptableComponent', () => {
describe('sync', () => {
it('should create a component from a ref for sync component', () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'random',
loader: () => (props: { name: string }) => {
return <div data-testid="test">{props.name}</div>;
@@ -83,7 +83,7 @@ describe('createAdaptableComponent', () => {
});
it('should render a fallback when theres no default implementation provided', () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'random',
});
@@ -93,7 +93,7 @@ describe('createAdaptableComponent', () => {
});
it('should map props from external to internal', () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'random',
transformProps: (props: { name: string }) => ({
uppercase: props.name.toUpperCase(),
@@ -114,7 +114,7 @@ describe('createAdaptableComponent', () => {
describe('async', () => {
it('should create a component from a ref for async component', async () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'random',
loader: async () => (props: { name: string }) => {
return <div data-testid="test">{props.name}</div>;
@@ -127,7 +127,7 @@ describe('createAdaptableComponent', () => {
});
it('should render a fallback when theres no default implementation provided', async () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'random',
});
@@ -137,7 +137,7 @@ describe('createAdaptableComponent', () => {
});
it('should map props from external to internal', async () => {
const Component = createAdaptableComponent({
const Component = createSwappableComponent({
id: 'random',
transformProps: (props: { name: string }) => ({
uppercase: props.name.toUpperCase(),
@@ -30,11 +30,11 @@ export type ComponentRef<
};
/**
* Options for creating an AdaptableComponent.
* Options for creating an SwappableComponent.
*
* @public
*/
export type CreateAdaptableComponentOptions<
export type CreateSwappableComponentOptions<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
> = {
@@ -96,15 +96,15 @@ function makeComponentFromRef<
}
/**
* Creates a AdaptableComponent that can be used to render the component, optionally overriden by the app.
* Creates a SwappableComponent that can be used to render the component, optionally overriden by the app.
*
* @public
*/
export function createAdaptableComponent<
export function createSwappableComponent<
TInnerComponentProps extends {},
TExternalComponentProps extends {} = TInnerComponentProps,
>(
options: CreateAdaptableComponentOptions<
options: CreateSwappableComponentOptions<
TInnerComponentProps,
TExternalComponentProps
>,
@@ -19,9 +19,9 @@ export {
type ExtensionBoundaryProps,
} from './ExtensionBoundary';
export {
createAdaptableComponent,
type CreateAdaptableComponentOptions,
createSwappableComponent,
type CreateSwappableComponentOptions,
type ComponentRef,
} from './createAdaptableComponent';
} from './createSwappableComponent';
export { useAppNode } from './AppNodeProvider';
export * from './DefaultAdaptableComponents';
export * from './DefaultSwappableComponents';
+3 -3
View File
@@ -15,7 +15,7 @@
*/
import {
AdaptableComponentBlueprint,
SwappableComponentBlueprint,
createExtensionInput,
ApiBlueprint,
componentsApiRef,
@@ -30,7 +30,7 @@ export const ComponentsApi = ApiBlueprint.makeWithOverrides({
name: 'components',
inputs: {
components: createExtensionInput(
[AdaptableComponentBlueprint.dataRefs.component],
[SwappableComponentBlueprint.dataRefs.component],
{ replaces: [{ id: 'app', input: 'components' }] },
),
},
@@ -42,7 +42,7 @@ export const ComponentsApi = ApiBlueprint.makeWithOverrides({
factory: () =>
DefaultComponentsApi.fromComponents(
inputs.components.map(i =>
i.get(AdaptableComponentBlueprint.dataRefs.component),
i.get(SwappableComponentBlueprint.dataRefs.component),
),
),
}),
+10 -10
View File
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import {
NotFoundErrorPage as AdaptableNotFoundErrorPage,
Progress as AdaptableProgress,
ErrorBoundary as AdaptableErrorBoundary,
AdaptableComponentBlueprint,
NotFoundErrorPage as SwappableNotFoundErrorPage,
Progress as SwappableProgress,
ErrorBoundary as SwappableErrorBoundary,
SwappableComponentBlueprint,
} from '@backstage/frontend-plugin-api';
import {
@@ -27,30 +27,30 @@ import {
} from '@backstage/core-components';
import Button from '@material-ui/core/Button';
export const Progress = AdaptableComponentBlueprint.make({
export const Progress = SwappableComponentBlueprint.make({
name: 'core.components.progress',
params: define =>
define({
component: AdaptableProgress,
component: SwappableProgress,
loader: () => ProgressComponent,
}),
});
export const NotFoundErrorPage = AdaptableComponentBlueprint.make({
export const NotFoundErrorPage = SwappableComponentBlueprint.make({
name: 'core.components.notFoundErrorPage',
params: define =>
define({
component: AdaptableNotFoundErrorPage,
component: SwappableNotFoundErrorPage,
loader: () => () =>
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />,
}),
});
export const ErrorBoundary = AdaptableComponentBlueprint.make({
export const ErrorBoundary = SwappableComponentBlueprint.make({
name: 'core.components.errorBoundary',
params: define =>
define({
component: AdaptableErrorBoundary,
component: SwappableErrorBoundary,
loader: () => props => {
const { plugin, error, resetError } = props;
const title = `Error in ${plugin?.id}`;