feat: big refactor of componentRefs again to move away from makeComponentRefs
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -14,9 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { createApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { ComponentRef } from '../../components';
|
||||
import { createApiRef } from '../system/ApiRef';
|
||||
|
||||
/**
|
||||
* API for looking up components based on component refs.
|
||||
@@ -24,8 +23,15 @@ import { ComponentRef } from '../../components';
|
||||
* @public
|
||||
*/
|
||||
export interface ComponentsApi {
|
||||
// TODO: Should component refs also provide the default implementation so that we're guaranteed to get a component?
|
||||
getComponent<T extends {}>(ref: ComponentRef<T>): ComponentType<T>;
|
||||
getComponent<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
>(
|
||||
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>,
|
||||
):
|
||||
| (() => (props: TInnerComponentProps) => JSX.Element | null)
|
||||
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,14 +42,3 @@ export interface ComponentsApi {
|
||||
export const componentsApiRef = createApiRef<ComponentsApi>({
|
||||
id: 'core.components',
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Returns the component associated with the given ref.
|
||||
*/
|
||||
export function useComponentRef<T extends {}>(
|
||||
ref: ComponentRef<T>,
|
||||
): ComponentType<T> {
|
||||
const componentsApi = useApi(componentsApiRef);
|
||||
return componentsApi.getComponent<T>(ref);
|
||||
}
|
||||
|
||||
+12
-21
@@ -14,23 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { renderInTestApp } from '@backstage/frontend-test-utils';
|
||||
import { createComponentRef } from '../components';
|
||||
import { makeComponentFromRef } from '../components/makeComponentFromRef';
|
||||
import { ComponentImplementationBlueprint } from './ComponentImplementationBlueprint';
|
||||
import { createAdaptableComponent } from '../components';
|
||||
import { AdaptableComponentBlueprint } from './AdaptableComponentBlueprint';
|
||||
import { PageBlueprint } from './PageBlueprint';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
|
||||
describe('ComponentImplementationBlueprint', () => {
|
||||
describe('AdaptableComponentBlueprint', () => {
|
||||
it('should allow defining a component override for a component ref', () => {
|
||||
const componentRef = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'test.component',
|
||||
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
|
||||
});
|
||||
|
||||
const extension = ComponentImplementationBlueprint.make({
|
||||
const extension = AdaptableComponentBlueprint.make({
|
||||
params: define =>
|
||||
define({
|
||||
ref: componentRef,
|
||||
component: Component,
|
||||
loader: () => props => {
|
||||
// @ts-expect-error
|
||||
const t: number = props.hello;
|
||||
@@ -44,13 +43,11 @@ describe('ComponentImplementationBlueprint', () => {
|
||||
});
|
||||
|
||||
it('should render default component refs in the app', async () => {
|
||||
const testComponentRef = createComponentRef({
|
||||
const TestComponent = createAdaptableComponent({
|
||||
id: 'test.component',
|
||||
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
|
||||
});
|
||||
|
||||
const TestComponent = makeComponentFromRef({ ref: testComponentRef });
|
||||
|
||||
renderInTestApp(<div />, {
|
||||
extensions: [
|
||||
PageBlueprint.make({
|
||||
@@ -69,12 +66,10 @@ describe('ComponentImplementationBlueprint', () => {
|
||||
});
|
||||
|
||||
it('should render a component ref without a default implementation', async () => {
|
||||
const testComponentRef = createComponentRef({
|
||||
const TestComponent = createAdaptableComponent({
|
||||
id: 'test.component',
|
||||
});
|
||||
|
||||
const TestComponent = makeComponentFromRef({ ref: testComponentRef });
|
||||
|
||||
renderInTestApp(<div />, {
|
||||
extensions: [
|
||||
PageBlueprint.make({
|
||||
@@ -94,14 +89,12 @@ describe('ComponentImplementationBlueprint', () => {
|
||||
});
|
||||
|
||||
it('should render a component ref with an async loader implementation', async () => {
|
||||
const testComponentRef = createComponentRef({
|
||||
const TestComponent = createAdaptableComponent({
|
||||
id: 'test.component',
|
||||
loader: async () => (props: { hello: string }) =>
|
||||
<div>{props.hello}</div>,
|
||||
});
|
||||
|
||||
const TestComponent = makeComponentFromRef({ ref: testComponentRef });
|
||||
|
||||
renderInTestApp(<div />, {
|
||||
extensions: [
|
||||
PageBlueprint.make({
|
||||
@@ -120,17 +113,15 @@ describe('ComponentImplementationBlueprint', () => {
|
||||
});
|
||||
|
||||
it('should allow overriding a component ref with the blueprint', async () => {
|
||||
const testComponentRef = createComponentRef({
|
||||
const TestComponent = createAdaptableComponent({
|
||||
id: 'test.component',
|
||||
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
|
||||
});
|
||||
|
||||
const TestComponent = makeComponentFromRef({ ref: testComponentRef });
|
||||
|
||||
const extension = ComponentImplementationBlueprint.make({
|
||||
const extension = AdaptableComponentBlueprint.make({
|
||||
params: define =>
|
||||
define({
|
||||
ref: testComponentRef,
|
||||
component: TestComponent,
|
||||
loader: () => props => <div>Override {props.hello}</div>,
|
||||
}),
|
||||
});
|
||||
+5
-3
@@ -27,7 +27,7 @@ export const componentDataRef = createExtensionDataRef<{
|
||||
| (() => Promise<(props: {}) => JSX.Element | null>);
|
||||
}>().with({ id: 'core.component.component' });
|
||||
|
||||
export const ComponentImplementationBlueprint = createExtensionBlueprint({
|
||||
export const AdaptableComponentBlueprint = createExtensionBlueprint({
|
||||
kind: 'component',
|
||||
attachTo: { id: 'api:app/components', input: 'components' },
|
||||
output: [componentDataRef],
|
||||
@@ -35,7 +35,9 @@ export const ComponentImplementationBlueprint = createExtensionBlueprint({
|
||||
component: componentDataRef,
|
||||
},
|
||||
defineParams<Ref extends ComponentRef<any>>(params: {
|
||||
ref: Ref;
|
||||
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)
|
||||
@@ -46,7 +48,7 @@ export const ComponentImplementationBlueprint = createExtensionBlueprint({
|
||||
},
|
||||
factory: params => [
|
||||
componentDataRef({
|
||||
ref: params.ref,
|
||||
ref: params.component.ref,
|
||||
loader: params.loader,
|
||||
}),
|
||||
],
|
||||
@@ -33,3 +33,4 @@ export { RouterBlueprint } from './RouterBlueprint';
|
||||
export { SignInPageBlueprint } from './SignInPageBlueprint';
|
||||
export { ThemeBlueprint } from './ThemeBlueprint';
|
||||
export { TranslationBlueprint } from './TranslationBlueprint';
|
||||
export { AdaptableComponentBlueprint } from './AdaptableComponentBlueprint';
|
||||
|
||||
@@ -25,8 +25,8 @@ import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker';
|
||||
import { AppNode, useComponentRef } from '../apis';
|
||||
import { coreComponentRefs } from './coreComponentRefs';
|
||||
import { AppNode } from '../apis';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
import { coreExtensionData } from '../wiring';
|
||||
import { AppNodeProvider } from './AppNodeProvider';
|
||||
|
||||
@@ -66,8 +66,9 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) {
|
||||
);
|
||||
|
||||
const plugin = node.spec.plugin;
|
||||
const Progress = useComponentRef(coreComponentRefs.progress);
|
||||
const fallback = useComponentRef(coreComponentRefs.errorBoundaryFallback);
|
||||
|
||||
// todo: fallback
|
||||
const fallback = () => <div>Fallback</div>;
|
||||
|
||||
// Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight
|
||||
const attributes = {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
CoreErrorBoundaryFallbackProps,
|
||||
CoreNotFoundErrorPageProps,
|
||||
CoreProgressProps,
|
||||
} from '../types';
|
||||
import { createComponentRef } from './createComponentRef';
|
||||
|
||||
const coreProgressComponentRef = createComponentRef<CoreProgressProps>({
|
||||
id: 'core.components.progress',
|
||||
});
|
||||
|
||||
const coreNotFoundErrorPageComponentRef =
|
||||
createComponentRef<CoreNotFoundErrorPageProps>({
|
||||
id: 'core.components.notFoundErrorPage',
|
||||
});
|
||||
|
||||
const coreErrorBoundaryFallbackComponentRef =
|
||||
createComponentRef<CoreErrorBoundaryFallbackProps>({
|
||||
id: 'core.components.errorBoundaryFallback',
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export const coreComponentRefs = {
|
||||
progress: coreProgressComponentRef,
|
||||
notFoundErrorPage: coreNotFoundErrorPageComponentRef,
|
||||
errorBoundaryFallback: coreErrorBoundaryFallbackComponentRef,
|
||||
};
|
||||
+58
-22
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
* 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.
|
||||
@@ -13,14 +13,61 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createComponentRef } from './createComponentRef';
|
||||
import { makeComponentFromRef } from './makeComponentFromRef';
|
||||
|
||||
describe('makeComponentFromRef', () => {
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createAdaptableComponent } from './createAdaptableComponent';
|
||||
|
||||
describe('createAdaptableComponent', () => {
|
||||
it('can be created and read', () => {
|
||||
const { ref } = createAdaptableComponent({ id: 'foo' });
|
||||
expect(ref.id).toBe('foo');
|
||||
expect(String(ref)).toBe('ComponentRef{id=foo}');
|
||||
});
|
||||
|
||||
it('should allow defining a default component implementation', () => {
|
||||
const Test = () => <div>test</div>;
|
||||
|
||||
createAdaptableComponent<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
loader:
|
||||
() =>
|
||||
({ foo }) =>
|
||||
<Test key={foo} />,
|
||||
});
|
||||
|
||||
createAdaptableComponent<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
loader:
|
||||
async () =>
|
||||
({ foo }) =>
|
||||
<Test key={foo} />,
|
||||
});
|
||||
|
||||
createAdaptableComponent<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
});
|
||||
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow transformings props', () => {
|
||||
createAdaptableComponent<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
transformProps: props => ({ foo: props.bar }),
|
||||
});
|
||||
|
||||
createAdaptableComponent<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
// @ts-expect-error - this should be an error as foo is not a string
|
||||
transformProps: props => ({ foo: 1 }),
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
describe('sync', () => {
|
||||
it('should create a component from a ref for sync component', () => {
|
||||
const ref = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'random',
|
||||
loader: () => (props: { name: string }) => {
|
||||
return <div data-testid="test">{props.name}</div>;
|
||||
@@ -30,26 +77,23 @@ describe('makeComponentFromRef', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
render(<Component id="test" />);
|
||||
|
||||
expect(screen.getByTestId('test')).toHaveTextContent('test');
|
||||
});
|
||||
|
||||
it('should render a fallback when theres no default implementation provided', () => {
|
||||
const ref = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'random',
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
|
||||
render(<Component />);
|
||||
|
||||
expect(screen.getByTestId('random')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should map props from external to internal', () => {
|
||||
const ref = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'random',
|
||||
transformProps: (props: { name: string }) => ({
|
||||
uppercase: props.name.toUpperCase(),
|
||||
@@ -62,8 +106,6 @@ describe('makeComponentFromRef', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
|
||||
render(<Component name="test" />);
|
||||
|
||||
expect(screen.getByTestId('test')).toHaveTextContent('TEST');
|
||||
@@ -72,34 +114,30 @@ describe('makeComponentFromRef', () => {
|
||||
|
||||
describe('async', () => {
|
||||
it('should create a component from a ref for async component', async () => {
|
||||
const ref = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'random',
|
||||
loader: async () => (props: { name: string }) => {
|
||||
return <div data-testid="test">{props.name}</div>;
|
||||
},
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
|
||||
render(<Component name="test" />);
|
||||
|
||||
await expect(screen.findByTestId('test')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a fallback when theres no default implementation provided', async () => {
|
||||
const ref = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'random',
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
|
||||
render(<Component />);
|
||||
|
||||
await expect(screen.findByTestId('random')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should map props from external to internal', async () => {
|
||||
const ref = createComponentRef({
|
||||
const Component = createAdaptableComponent({
|
||||
id: 'random',
|
||||
transformProps: (props: { name: string }) => ({
|
||||
uppercase: props.name.toUpperCase(),
|
||||
@@ -112,8 +150,6 @@ describe('makeComponentFromRef', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
|
||||
render(<Component name="test" />);
|
||||
|
||||
await expect(screen.findByTestId('test')).resolves.toHaveTextContent(
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 { OpaqueComponentRef } from '@internal/frontend';
|
||||
import { componentsApiRef, useApi } from '../apis';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
/** @public */
|
||||
export type ComponentRef<
|
||||
TInnerComponentProps extends {} = {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
> = {
|
||||
id: string;
|
||||
TProps: TInnerComponentProps;
|
||||
TExternalProps: TExternalComponentProps;
|
||||
$$type: '@backstage/ComponentRef';
|
||||
};
|
||||
|
||||
export type ComponentRefOptions<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
> = {
|
||||
id: string;
|
||||
loader?:
|
||||
| (() => (props: TInnerComponentProps) => JSX.Element | null)
|
||||
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>);
|
||||
transformProps?: (props: TExternalComponentProps) => TInnerComponentProps;
|
||||
};
|
||||
|
||||
const useComponentRefApi = () => {
|
||||
try {
|
||||
return useApi(componentsApiRef);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
function makeComponentFromRef<
|
||||
InternalComponentProps extends {},
|
||||
ExternalComponentProps extends {},
|
||||
>({
|
||||
ref,
|
||||
}: {
|
||||
ref: ComponentRef<InternalComponentProps, ExternalComponentProps>;
|
||||
}): (props: ExternalComponentProps) => JSX.Element {
|
||||
const internalRef = OpaqueComponentRef.toInternal(ref);
|
||||
const FallbackComponent = (p: JSX.IntrinsicAttributes) => (
|
||||
<div data-testid={ref.id} {...p} />
|
||||
);
|
||||
|
||||
const ComponentRefImpl = (props: ExternalComponentProps) => {
|
||||
const api = useComponentRefApi();
|
||||
const ComponentOrPromise =
|
||||
api?.getComponent<any>(ref)?.() ??
|
||||
internalRef.loader?.() ??
|
||||
FallbackComponent;
|
||||
|
||||
const innerProps = internalRef.transformProps?.(props) ?? props;
|
||||
|
||||
if ('then' in ComponentOrPromise) {
|
||||
const DefaultImplementation = lazy(() =>
|
||||
ComponentOrPromise.then(c => {
|
||||
return { default: c };
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<DefaultImplementation {...innerProps} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <ComponentOrPromise {...innerProps} />;
|
||||
};
|
||||
|
||||
return ComponentRefImpl;
|
||||
}
|
||||
|
||||
export function createAdaptableComponent<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
>(
|
||||
options: ComponentRefOptions<TInnerComponentProps, TExternalComponentProps>,
|
||||
): ((props: TExternalComponentProps) => JSX.Element) & {
|
||||
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>;
|
||||
} {
|
||||
const ref = OpaqueComponentRef.createInstance('v1', {
|
||||
id: options.id,
|
||||
TProps: null as unknown as TInnerComponentProps,
|
||||
TExternalProps: null as unknown as TExternalComponentProps,
|
||||
toString() {
|
||||
return `ComponentRef{id=${options.id}}`;
|
||||
},
|
||||
loader: options.loader as (typeof OpaqueComponentRef.TInternal)['loader'],
|
||||
transformProps:
|
||||
options.transformProps as (typeof OpaqueComponentRef.TInternal)['transformProps'],
|
||||
});
|
||||
|
||||
const component = makeComponentFromRef({ ref });
|
||||
Object.assign(component, { ref });
|
||||
|
||||
return component as {
|
||||
ref: ComponentRef<TInnerComponentProps, TExternalComponentProps>;
|
||||
} & ((props: object) => JSX.Element);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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 { createComponentRef } from './createComponentRef';
|
||||
|
||||
describe('createComponentRef', () => {
|
||||
it('can be created and read', () => {
|
||||
const ref = createComponentRef({ id: 'foo' });
|
||||
expect(ref.id).toBe('foo');
|
||||
expect(String(ref)).toBe('ComponentRef{id=foo}');
|
||||
});
|
||||
|
||||
it('should allow defining a default component implementation', () => {
|
||||
const Test = () => <div>test</div>;
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
loader:
|
||||
() =>
|
||||
({ foo }) =>
|
||||
<Test key={foo} />,
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
loader:
|
||||
async () =>
|
||||
({ foo }) =>
|
||||
<Test key={foo} />,
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
});
|
||||
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow transformings props', () => {
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
transformProps: props => ({ foo: props.bar }),
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
// @ts-expect-error - this should be an error as foo is not a string
|
||||
transformProps: props => ({ foo: 1 }),
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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 { OpaqueComponentRef } from '@internal/frontend';
|
||||
|
||||
/** @public */
|
||||
export type ComponentRef<
|
||||
TInnerComponentProps extends {} = {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
> = {
|
||||
id: string;
|
||||
TProps: TInnerComponentProps;
|
||||
TExternalProps: TExternalComponentProps;
|
||||
$$type: '@backstage/ComponentRef';
|
||||
};
|
||||
|
||||
export type ComponentRefOptions<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
> = {
|
||||
id: string;
|
||||
loader?:
|
||||
| (() => (props: TInnerComponentProps) => JSX.Element | null)
|
||||
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>);
|
||||
transformProps?: (props: TExternalComponentProps) => TInnerComponentProps;
|
||||
};
|
||||
|
||||
export function createComponentRef<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
>(
|
||||
options: ComponentRefOptions<TInnerComponentProps, TExternalComponentProps>,
|
||||
): ComponentRef<TInnerComponentProps, TExternalComponentProps> {
|
||||
return OpaqueComponentRef.createInstance('v1', {
|
||||
id: options.id,
|
||||
TProps: null as unknown as TInnerComponentProps,
|
||||
TExternalProps: null as unknown as TExternalComponentProps,
|
||||
toString() {
|
||||
return `ComponentRef{id=${options.id}}`;
|
||||
},
|
||||
options: {
|
||||
loader: options.loader,
|
||||
transformProps: options.transformProps,
|
||||
} as (typeof OpaqueComponentRef.TInternal)['options'],
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,8 @@ export {
|
||||
ExtensionBoundary,
|
||||
type ExtensionBoundaryProps,
|
||||
} from './ExtensionBoundary';
|
||||
export { coreComponentRefs } from './coreComponentRefs';
|
||||
export { createComponentRef, type ComponentRef } from './createComponentRef';
|
||||
export {
|
||||
createAdaptableComponent,
|
||||
type ComponentRef,
|
||||
} from './createAdaptableComponent';
|
||||
export { useAppNode } from './AppNodeProvider';
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* 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 { lazy, Suspense } from 'react';
|
||||
import { ComponentRef } from './createComponentRef';
|
||||
import { OpaqueComponentRef } from '@internal/frontend';
|
||||
import { componentsApiRef, useApi } from '../apis';
|
||||
|
||||
export function makeComponentFromRef<
|
||||
InternalComponentProps extends {},
|
||||
ExternalComponentProps extends {},
|
||||
>({
|
||||
ref,
|
||||
}: {
|
||||
ref: ComponentRef<InternalComponentProps, ExternalComponentProps>;
|
||||
}): (props: ExternalComponentProps) => JSX.Element {
|
||||
const { options } = OpaqueComponentRef.toInternal(ref);
|
||||
const FallbackComponent = (p: JSX.IntrinsicAttributes) => (
|
||||
<div data-testid={ref.id} {...p} />
|
||||
);
|
||||
|
||||
const ComponentRefImpl = (props: ExternalComponentProps) => {
|
||||
// todo(blam): use the component that's in the API ref instead if it's defined.
|
||||
// otherwise use the fallback..
|
||||
const api = useApi(componentsApiRef);
|
||||
const innerProps = options.transformProps?.(props) ?? props;
|
||||
|
||||
const ComponentOrPromise = options.loader?.() ?? FallbackComponent;
|
||||
|
||||
if ('then' in ComponentOrPromise) {
|
||||
const DefaultImplementation = lazy(() =>
|
||||
ComponentOrPromise.then(c => {
|
||||
return { default: c };
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
// todo: is this necessary? can we remove this?
|
||||
<Suspense>
|
||||
<DefaultImplementation {...innerProps} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <ComponentOrPromise {...innerProps} />;
|
||||
};
|
||||
|
||||
return ComponentRefImpl;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* 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 { lazy, ComponentType } from 'react';
|
||||
import { createExtension, createExtensionDataRef } from '../wiring';
|
||||
import { ComponentRef } from '../components';
|
||||
|
||||
/** @public */
|
||||
export function createComponentExtension<TProps extends {}>(options: {
|
||||
ref: ComponentRef<TProps>;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
loader:
|
||||
| {
|
||||
lazy: () => Promise<ComponentType<TProps>>;
|
||||
}
|
||||
| {
|
||||
sync: () => ComponentType<TProps>;
|
||||
};
|
||||
}) {
|
||||
return createExtension({
|
||||
kind: 'component',
|
||||
name: options.name ?? options.ref.id,
|
||||
attachTo: { id: 'api:app/components', input: 'components' },
|
||||
disabled: options.disabled,
|
||||
output: [createComponentExtension.componentDataRef],
|
||||
factory() {
|
||||
if ('sync' in options.loader) {
|
||||
return [
|
||||
createComponentExtension.componentDataRef({
|
||||
ref: options.ref,
|
||||
impl: options.loader.sync() as ComponentType,
|
||||
}),
|
||||
];
|
||||
}
|
||||
const lazyLoader = options.loader.lazy;
|
||||
const ExtensionComponent = lazy(() =>
|
||||
lazyLoader().then(Component => ({
|
||||
default: Component,
|
||||
})),
|
||||
) as unknown as ComponentType;
|
||||
|
||||
return [
|
||||
createComponentExtension.componentDataRef({
|
||||
ref: options.ref,
|
||||
impl: ExtensionComponent,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export namespace createComponentExtension {
|
||||
export const componentDataRef = createExtensionDataRef<{
|
||||
ref: ComponentRef;
|
||||
impl: ComponentType;
|
||||
}>().with({ id: 'core.component.component' });
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 { createComponentExtension } from './createComponentExtension';
|
||||
@@ -24,7 +24,6 @@ export * from './analytics';
|
||||
export * from './apis';
|
||||
export * from './blueprints';
|
||||
export * from './components';
|
||||
export * from './extensions';
|
||||
export * from './icons';
|
||||
export * from './routing';
|
||||
export * from './schema';
|
||||
|
||||
Reference in New Issue
Block a user