feat: big refactor of componentRefs again to move away from makeComponentRefs
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -14,13 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createComponentExtension,
|
||||
coreComponentRefs,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { AdaptableComponentBlueprint } 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';
|
||||
import { NotFoundErrorPage } from '@backstage/plugin-app';
|
||||
|
||||
export function CustomNotFoundErrorPage() {
|
||||
return (
|
||||
@@ -50,8 +48,11 @@ export function CustomNotFoundErrorPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default createComponentExtension({
|
||||
export default AdaptableComponentBlueprint.make({
|
||||
name: 'not-found-error-page',
|
||||
ref: coreComponentRefs.notFoundErrorPage,
|
||||
loader: { sync: () => CustomNotFoundErrorPage },
|
||||
params: define =>
|
||||
define({
|
||||
component: NotFoundErrorPage,
|
||||
loader: () => CustomNotFoundErrorPage,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
createFrontendPlugin as createNewPlugin,
|
||||
FrontendPlugin as NewFrontendPlugin,
|
||||
appTreeApiRef,
|
||||
componentsApiRef,
|
||||
coreComponentRefs,
|
||||
iconsApiRef,
|
||||
useApi,
|
||||
routeResolutionApiRef,
|
||||
@@ -44,6 +42,9 @@ import {
|
||||
} from '@backstage/version-bridge';
|
||||
import { convertLegacyRouteRef } from '../convertLegacyRouteRef';
|
||||
|
||||
import { ErrorBoundary, NotFoundErrorPage } from '@backstage/plugin-app';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
|
||||
// Make sure that we only convert each new plugin instance to its legacy equivalent once
|
||||
const legacyPluginStore = getOrCreateGlobalSingleton(
|
||||
'legacy-plugin-compatibility-store',
|
||||
@@ -92,7 +93,6 @@ function toNewPlugin(plugin: LegacyBackstagePlugin): NewFrontendPlugin {
|
||||
// Recreates the old AppContext APIs using the various new APIs that replaced it
|
||||
function LegacyAppContextProvider(props: { children: ReactNode }) {
|
||||
const appTreeApi = useApi(appTreeApiRef);
|
||||
const componentsApi = useApi(componentsApiRef);
|
||||
const iconsApi = useApi(iconsApiRef);
|
||||
|
||||
const appContext = useMemo(() => {
|
||||
@@ -100,15 +100,9 @@ function LegacyAppContextProvider(props: { children: ReactNode }) {
|
||||
|
||||
let gatheredPlugins: LegacyBackstagePlugin[] | undefined = undefined;
|
||||
|
||||
const ErrorBoundaryFallback = componentsApi.getComponent(
|
||||
coreComponentRefs.errorBoundaryFallback,
|
||||
);
|
||||
const ErrorBoundaryFallbackWrapper: AppComponents['ErrorBoundaryFallback'] =
|
||||
({ plugin, ...rest }) => (
|
||||
<ErrorBoundaryFallback
|
||||
{...rest}
|
||||
plugin={plugin && toNewPlugin(plugin)}
|
||||
/>
|
||||
<ErrorBoundary {...rest} plugin={plugin && toNewPlugin(plugin)} />
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -141,15 +135,13 @@ function LegacyAppContextProvider(props: { children: ReactNode }) {
|
||||
|
||||
getComponents(): AppComponents {
|
||||
return {
|
||||
NotFoundErrorPage: componentsApi.getComponent(
|
||||
coreComponentRefs.notFoundErrorPage,
|
||||
),
|
||||
NotFoundErrorPage: NotFoundErrorPage,
|
||||
BootErrorPage() {
|
||||
throw new Error(
|
||||
'The BootErrorPage app component should not be accessed by plugins',
|
||||
);
|
||||
},
|
||||
Progress: componentsApi.getComponent(coreComponentRefs.progress),
|
||||
Progress: Progress,
|
||||
Router() {
|
||||
throw new Error(
|
||||
'The Router app component should not be accessed by plugins',
|
||||
@@ -159,7 +151,7 @@ function LegacyAppContextProvider(props: { children: ReactNode }) {
|
||||
};
|
||||
},
|
||||
};
|
||||
}, [appTreeApi, componentsApi, iconsApi]);
|
||||
}, [appTreeApi, iconsApi]);
|
||||
|
||||
return (
|
||||
<AppContextProvider appContext={appContext}>
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
AdaptableComponentBlueprint,
|
||||
ApiBlueprint,
|
||||
coreComponentRefs,
|
||||
CoreErrorBoundaryFallbackProps,
|
||||
createComponentExtension,
|
||||
createExtension,
|
||||
createFrontendModule,
|
||||
ExtensionDefinition,
|
||||
@@ -39,6 +38,11 @@ import {
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { toLegacyPlugin } from './compatWrapper/BackwardsCompatProvider';
|
||||
import { compatWrapper } from './compatWrapper';
|
||||
import { Progress as AdaptableProgress } from '@backstage/core-components';
|
||||
import {
|
||||
NotFoundErrorPage as AdaptableNotFoundErrorPage,
|
||||
ErrorBoundary as AdaptableErrorBoundary,
|
||||
} from '@backstage/plugin-app';
|
||||
|
||||
function componentCompatWrapper<TProps extends {}>(
|
||||
Component: ComponentType<TProps>,
|
||||
@@ -154,20 +158,27 @@ export function convertLegacyAppOptions(
|
||||
}
|
||||
if (Progress) {
|
||||
extensions.push(
|
||||
createComponentExtension({
|
||||
ref: coreComponentRefs.progress,
|
||||
loader: { sync: () => componentCompatWrapper(Progress) },
|
||||
AdaptableComponentBlueprint.make({
|
||||
params: define =>
|
||||
define({
|
||||
component: AdaptableProgress,
|
||||
loader: () => componentCompatWrapper(Progress),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (NotFoundErrorPage) {
|
||||
extensions.push(
|
||||
createComponentExtension({
|
||||
ref: coreComponentRefs.notFoundErrorPage,
|
||||
loader: { sync: () => componentCompatWrapper(NotFoundErrorPage) },
|
||||
AdaptableComponentBlueprint.make({
|
||||
params: define =>
|
||||
define({
|
||||
component: AdaptableNotFoundErrorPage,
|
||||
loader: () => componentCompatWrapper(NotFoundErrorPage),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (ErrorBoundaryFallback) {
|
||||
const WrappedErrorBoundaryFallback = (
|
||||
props: CoreErrorBoundaryFallbackProps,
|
||||
@@ -178,12 +189,16 @@ export function convertLegacyAppOptions(
|
||||
plugin={props.plugin && toLegacyPlugin(props.plugin)}
|
||||
/>,
|
||||
);
|
||||
|
||||
extensions.push(
|
||||
createComponentExtension({
|
||||
ref: coreComponentRefs.errorBoundaryFallback,
|
||||
loader: {
|
||||
sync: () => componentCompatWrapper(WrappedErrorBoundaryFallback),
|
||||
},
|
||||
AdaptableComponentBlueprint.make({
|
||||
params: define =>
|
||||
define({
|
||||
component: AdaptableErrorBoundary,
|
||||
loader: () =>
|
||||
// todo: types + props lols
|
||||
componentCompatWrapper(WrappedErrorBoundaryFallback),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ import LinearProgress, {
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||
|
||||
export function Progress(props: PropsWithChildren<LinearProgressProps>) {
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { createAdaptableComponent } from '../../../../frontend-plugin-api/src/components/createAdaptableComponent';
|
||||
|
||||
function ProgressComponent(props: PropsWithChildren<LinearProgressProps>) {
|
||||
const theme = useTheme();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
@@ -39,3 +42,8 @@ export function Progress(props: PropsWithChildren<LinearProgressProps>) {
|
||||
<Box display="none" data-testid="progress" />
|
||||
);
|
||||
}
|
||||
|
||||
export const Progress = createAdaptableComponent({
|
||||
id: 'core.components.progress',
|
||||
loader: () => ProgressComponent,
|
||||
});
|
||||
|
||||
+10
-9
@@ -14,24 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createComponentRef } from '@backstage/frontend-plugin-api';
|
||||
import { createAdaptableComponent } 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 { ref: testRefA } = createAdaptableComponent({ id: 'test.a' });
|
||||
const { ref: testRefB1 } = createAdaptableComponent({ id: 'test.b' });
|
||||
const { ref: testRefB2 } = createAdaptableComponent({ id: 'test.b' });
|
||||
|
||||
describe('DefaultComponentsApi', () => {
|
||||
it('should provide components', () => {
|
||||
const api = DefaultComponentsApi.fromComponents([
|
||||
{
|
||||
ref: testRefA,
|
||||
impl: () => <div>test.a</div>,
|
||||
loader: () => () => <div>test.a</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const ComponentA = api.getComponent(testRefA);
|
||||
const ComponentA = api.getComponent(testRefA)?.() as () => JSX.Element;
|
||||
|
||||
render(<ComponentA />);
|
||||
|
||||
expect(screen.getByText('test.a')).toBeInTheDocument();
|
||||
@@ -41,12 +42,12 @@ describe('DefaultComponentsApi', () => {
|
||||
const api = DefaultComponentsApi.fromComponents([
|
||||
{
|
||||
ref: testRefB1,
|
||||
impl: () => <div>test.b</div>,
|
||||
loader: () => () => <div>test.b</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const ComponentB1 = api.getComponent(testRefB1);
|
||||
const ComponentB2 = api.getComponent(testRefB2);
|
||||
const ComponentB1 = api.getComponent(testRefB1)?.() as () => JSX.Element;
|
||||
const ComponentB2 = api.getComponent(testRefB2)?.() as () => JSX.Element;
|
||||
|
||||
expect(ComponentB1).toBe(ComponentB2);
|
||||
|
||||
|
||||
+15
-9
@@ -14,11 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
ComponentRef,
|
||||
ComponentsApi,
|
||||
createComponentExtension,
|
||||
AdaptableComponentBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
@@ -27,13 +26,18 @@ import {
|
||||
* @internal
|
||||
*/
|
||||
export class DefaultComponentsApi implements ComponentsApi {
|
||||
#components: Map<string, ComponentType<any>>;
|
||||
#components: Map<
|
||||
string,
|
||||
| (() => (props: object) => JSX.Element | null)
|
||||
| (() => Promise<(props: object) => JSX.Element | null>)
|
||||
| undefined
|
||||
>;
|
||||
|
||||
static fromComponents(
|
||||
components: Array<typeof createComponentExtension.componentDataRef.T>,
|
||||
components: Array<typeof AdaptableComponentBlueprint.dataRefs.component.T>,
|
||||
) {
|
||||
return new DefaultComponentsApi(
|
||||
new Map(components.map(entry => [entry.ref.id, entry.impl])),
|
||||
new Map(components.map(entry => [entry.ref.id, entry.loader])),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,11 +45,13 @@ export class DefaultComponentsApi implements ComponentsApi {
|
||||
this.#components = components;
|
||||
}
|
||||
|
||||
getComponent<T extends {}>(ref: ComponentRef<T>): ComponentType<T> {
|
||||
getComponent(
|
||||
ref: ComponentRef<any>,
|
||||
):
|
||||
| (() => (props: object) => JSX.Element | null)
|
||||
| (() => Promise<(props: object) => JSX.Element | null>)
|
||||
| undefined {
|
||||
const impl = this.#components.get(ref.id);
|
||||
if (!impl) {
|
||||
throw new Error(`No implementation found for component ref ${ref}`);
|
||||
}
|
||||
return impl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,10 @@ export const OpaqueComponentRef = OpaqueType.create<{
|
||||
public: ComponentRef;
|
||||
versions: {
|
||||
readonly version: 'v1';
|
||||
readonly options: {
|
||||
transformProps?: (props: object) => object;
|
||||
loader?:
|
||||
| (() => (props: object) => JSX.Element | null)
|
||||
| (() => Promise<(props: object) => JSX.Element | null>);
|
||||
};
|
||||
readonly transformProps?: (props: object) => object;
|
||||
readonly loader?:
|
||||
| (() => (props: object) => JSX.Element | null)
|
||||
| (() => Promise<(props: object) => JSX.Element | null>);
|
||||
};
|
||||
}>({
|
||||
versions: ['v1'],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -18,10 +18,9 @@ import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
coreComponentRefs,
|
||||
useComponentRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import { NotFoundErrorPage } from './components';
|
||||
|
||||
export const AppRoutes = createExtension({
|
||||
name: 'routes',
|
||||
@@ -36,10 +35,6 @@ export const AppRoutes = createExtension({
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs }) {
|
||||
const Routes = () => {
|
||||
const NotFoundErrorPage = useComponentRef(
|
||||
coreComponentRefs.notFoundErrorPage,
|
||||
);
|
||||
|
||||
const element = useRoutes([
|
||||
...inputs.routes.map(route => ({
|
||||
path: `${route
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createComponentExtension,
|
||||
AdaptableComponentBlueprint,
|
||||
createExtensionInput,
|
||||
ApiBlueprint,
|
||||
componentsApiRef,
|
||||
@@ -30,7 +30,7 @@ export const ComponentsApi = ApiBlueprint.makeWithOverrides({
|
||||
name: 'components',
|
||||
inputs: {
|
||||
components: createExtensionInput(
|
||||
[createComponentExtension.componentDataRef],
|
||||
[AdaptableComponentBlueprint.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(createComponentExtension.componentDataRef),
|
||||
i.get(AdaptableComponentBlueprint.dataRefs.component),
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -14,40 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { createAdaptableComponent } from '@backstage/frontend-plugin-api';
|
||||
|
||||
import {
|
||||
createComponentExtension,
|
||||
coreComponentRefs,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { components as defaultComponents } from '../../../../packages/app-defaults/src/defaults';
|
||||
|
||||
export const DefaultProgressComponent = createComponentExtension({
|
||||
ref: coreComponentRefs.progress,
|
||||
loader: { sync: () => defaultComponents.Progress },
|
||||
export const NotFoundErrorPage = createAdaptableComponent({
|
||||
id: 'core.components.notFoundErrorPage',
|
||||
// todo: implementation
|
||||
});
|
||||
|
||||
export const DefaultNotFoundErrorPageComponent = createComponentExtension({
|
||||
ref: coreComponentRefs.notFoundErrorPage,
|
||||
loader: { sync: () => defaultComponents.NotFoundErrorPage },
|
||||
});
|
||||
|
||||
export const DefaultErrorBoundaryComponent = createComponentExtension({
|
||||
ref: coreComponentRefs.errorBoundaryFallback,
|
||||
loader: {
|
||||
sync: () => props => {
|
||||
const { plugin, error, resetError } = props;
|
||||
const title = `Error in ${plugin?.id}`;
|
||||
|
||||
return (
|
||||
<ErrorPanel title={title} error={error} defaultExpanded>
|
||||
<Button variant="outlined" onClick={resetError}>
|
||||
Retry
|
||||
</Button>
|
||||
</ErrorPanel>
|
||||
);
|
||||
},
|
||||
},
|
||||
export const ErrorBoundary = createAdaptableComponent({
|
||||
id: 'core.components.errorBoundaryFallback',
|
||||
// todo: implementation
|
||||
});
|
||||
|
||||
@@ -26,11 +26,8 @@ export { FeatureFlagsApi } from './FeatureFlagsApi';
|
||||
export { TranslationsApi } from './TranslationsApi';
|
||||
export { DefaultSignInPage } from './DefaultSignInPage';
|
||||
export { dialogDisplayAppRootElement } from './DialogDisplay';
|
||||
export {
|
||||
DefaultProgressComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
DefaultNotFoundErrorPageComponent,
|
||||
} from './components';
|
||||
// todo: move these
|
||||
export { ErrorBoundary, NotFoundErrorPage } from './components';
|
||||
export {
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
*/
|
||||
|
||||
export { appPlugin as default } from './plugin';
|
||||
// todo: remove
|
||||
export * from './extensions/components';
|
||||
|
||||
@@ -29,9 +29,6 @@ import {
|
||||
IconsApi,
|
||||
FeatureFlagsApi,
|
||||
TranslationsApi,
|
||||
DefaultProgressComponent,
|
||||
DefaultNotFoundErrorPageComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
DefaultSignInPage,
|
||||
@@ -58,9 +55,6 @@ export const appPlugin = createFrontendPlugin({
|
||||
IconsApi,
|
||||
FeatureFlagsApi,
|
||||
TranslationsApi,
|
||||
DefaultProgressComponent,
|
||||
DefaultNotFoundErrorPageComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
DefaultSignInPage,
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
|
||||
Reference in New Issue
Block a user