frontend-app-api: key components by id instead of ref + tests
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
The default `ComponentsApi` implementation now uses the `ComponentRef` ID as the component key, rather than the reference instance. This fixes a bug where duplicate installations of `@backstage/frontend-plugin-api` would break the app.
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createComponentExtension,
|
||||
createComponentRef,
|
||||
createExtension,
|
||||
createExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { resolveAppNodeSpecs } from '../../../tree/resolveAppNodeSpecs';
|
||||
import { resolveAppTree } from '../../../tree/resolveAppTree';
|
||||
import { App } from '../../../extensions/App';
|
||||
import { DefaultComponentsApi } from './DefaultComponentsApi';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { instantiateAppNodeTree } from '../../../tree/instantiateAppNodeTree';
|
||||
|
||||
const testRefA = createComponentRef({ id: 'test.a' });
|
||||
const testRefB1 = createComponentRef({ id: 'test.b' });
|
||||
const testRefB2 = createComponentRef({ id: 'test.b' });
|
||||
|
||||
const baseOverrides = createExtensionOverrides({
|
||||
extensions: [
|
||||
App,
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'root',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory() {
|
||||
return {
|
||||
element: <div>root</div>,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe('DefaultComponentsApi', () => {
|
||||
it('should provide components', () => {
|
||||
const tree = resolveAppTree(
|
||||
'app',
|
||||
resolveAppNodeSpecs({
|
||||
features: [
|
||||
baseOverrides,
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createComponentExtension({
|
||||
ref: testRefA,
|
||||
loader: { sync: () => () => <div>test.a</div> },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
instantiateAppNodeTree(tree.root);
|
||||
const api = DefaultComponentsApi.fromTree(tree);
|
||||
|
||||
const ComponentA = api.getComponent(testRefA);
|
||||
render(<ComponentA />);
|
||||
|
||||
expect(screen.getByText('test.a')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should key extension refs by ID', () => {
|
||||
const tree = resolveAppTree(
|
||||
'app',
|
||||
resolveAppNodeSpecs({
|
||||
features: [
|
||||
baseOverrides,
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createComponentExtension({
|
||||
ref: testRefB1,
|
||||
loader: { sync: () => () => <div>test.b</div> },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
instantiateAppNodeTree(tree.root);
|
||||
const api = DefaultComponentsApi.fromTree(tree);
|
||||
|
||||
const ComponentB1 = api.getComponent(testRefB1);
|
||||
const ComponentB2 = api.getComponent(testRefB2);
|
||||
|
||||
expect(ComponentB1).toBe(ComponentB2);
|
||||
|
||||
render(<ComponentB2 />);
|
||||
|
||||
expect(screen.getByText('test.b')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+24
-4
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { ComponentRef, ComponentsApi } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
AppTree,
|
||||
ComponentRef,
|
||||
ComponentsApi,
|
||||
createComponentExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
* Implementation for the {@linkComponentApi}
|
||||
@@ -23,14 +28,29 @@ import { ComponentRef, ComponentsApi } from '@backstage/frontend-plugin-api';
|
||||
* @internal
|
||||
*/
|
||||
export class DefaultComponentsApi implements ComponentsApi {
|
||||
#components: Map<ComponentRef<any>, ComponentType<any>>;
|
||||
#components: Map<string, ComponentType<any>>;
|
||||
|
||||
constructor(components: Map<ComponentRef<any>, any>) {
|
||||
static fromTree(tree: AppTree) {
|
||||
const componentEntries = tree.root.edges.attachments
|
||||
.get('components')
|
||||
?.reduce((map, e) => {
|
||||
const data = e.instance?.getData(
|
||||
createComponentExtension.componentDataRef,
|
||||
);
|
||||
if (data) {
|
||||
map.set(data.ref.id, data.impl);
|
||||
}
|
||||
return map;
|
||||
}, new Map<string, ComponentType>());
|
||||
return new DefaultComponentsApi(componentEntries ?? new Map());
|
||||
}
|
||||
|
||||
constructor(components: Map<string, any>) {
|
||||
this.#components = components;
|
||||
}
|
||||
|
||||
getComponent<T extends {}>(ref: ComponentRef<T>): ComponentType<T> {
|
||||
const impl = this.#components.get(ref);
|
||||
const impl = this.#components.get(ref.id);
|
||||
if (!impl) {
|
||||
throw new Error(`No implementation found for component ref ${ref}`);
|
||||
}
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DefaultComponentsApi } from './ComponentsApi';
|
||||
export { DefaultComponentsApi } from './DefaultComponentsApi';
|
||||
|
||||
@@ -31,17 +31,22 @@ import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/res
|
||||
|
||||
/** @internal */
|
||||
export function resolveAppNodeSpecs(options: {
|
||||
features: FrontendFeature[];
|
||||
builtinExtensions: Extension<unknown>[];
|
||||
parameters: Array<ExtensionParameters>;
|
||||
features?: FrontendFeature[];
|
||||
builtinExtensions?: Extension<unknown>[];
|
||||
parameters?: Array<ExtensionParameters>;
|
||||
forbidden?: Set<string>;
|
||||
}): AppNodeSpec[] {
|
||||
const { builtinExtensions, parameters, forbidden = new Set() } = options;
|
||||
const {
|
||||
builtinExtensions = [],
|
||||
parameters = [],
|
||||
forbidden = new Set(),
|
||||
features = [],
|
||||
} = options;
|
||||
|
||||
const plugins = options.features.filter(
|
||||
const plugins = features.filter(
|
||||
(f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
|
||||
);
|
||||
const overrides = options.features.filter(
|
||||
const overrides = features.filter(
|
||||
(f): f is ExtensionOverrides =>
|
||||
f.$$type === '@backstage/ExtensionOverrides',
|
||||
);
|
||||
|
||||
@@ -19,11 +19,9 @@ import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
AppTree,
|
||||
appTreeApiRef,
|
||||
ComponentRef,
|
||||
componentsApiRef,
|
||||
coreExtensionData,
|
||||
createApiExtension,
|
||||
createComponentExtension,
|
||||
createThemeExtension,
|
||||
createTranslationExtension,
|
||||
FrontendFeature,
|
||||
@@ -373,22 +371,10 @@ function createApiHolder(
|
||||
factory: () => routeResolutionApi,
|
||||
});
|
||||
|
||||
const componentsExtensions =
|
||||
tree.root.edges.attachments
|
||||
.get('components')
|
||||
?.map(e => e.instance?.getData(createComponentExtension.componentDataRef))
|
||||
.filter(x => !!x) ?? [];
|
||||
|
||||
const componentsMap = componentsExtensions.reduce(
|
||||
(components, component) =>
|
||||
component ? components.set(component.ref, component?.impl) : components,
|
||||
new Map<ComponentRef<any>, any>(),
|
||||
);
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: componentsApiRef,
|
||||
deps: {},
|
||||
factory: () => new DefaultComponentsApi(componentsMap),
|
||||
factory: () => DefaultComponentsApi.fromTree(tree),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
|
||||
Reference in New Issue
Block a user