feat: simplify the extension redirects

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-23 17:03:40 +02:00
parent ba3d5357cd
commit a5ee952079
7 changed files with 113 additions and 40 deletions
@@ -36,7 +36,9 @@ import { AppThemeSelector } from '@backstage/core-app-api';
export const AppThemeApi = ApiBlueprint.makeWithOverrides({
name: 'app-theme',
inputs: {
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]),
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme], {
replaces: [{ id: 'app', input: 'themes' }],
}),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
@@ -29,9 +29,10 @@ import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
export const ComponentsApi = ApiBlueprint.makeWithOverrides({
name: 'components',
inputs: {
components: createExtensionInput([
createComponentExtension.componentDataRef,
]),
components: createExtensionInput(
[createComponentExtension.componentDataRef],
{ replaces: [{ id: 'app', input: 'components' }] },
),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
@@ -31,7 +31,9 @@ import { icons as defaultIcons } from '../../../app-defaults/src/defaults';
export const IconsApi = ApiBlueprint.makeWithOverrides({
name: 'icons',
inputs: {
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]),
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons], {
replaces: [{ id: 'app', input: 'icons' }],
}),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
@@ -33,9 +33,10 @@ import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementa
export const TranslationsApi = ApiBlueprint.makeWithOverrides({
name: 'translations',
inputs: {
translations: createExtensionInput([
TranslationBlueprint.dataRefs.translation,
]),
translations: createExtensionInput(
[TranslationBlueprint.dataRefs.translation],
{ replaces: [{ id: 'app', input: 'translations' }] },
),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
@@ -15,6 +15,7 @@
*/
import {
AppNodeSpec,
coreExtensionData,
createExtension,
createExtensionInput,
@@ -208,38 +209,37 @@ describe('buildAppTree', () => {
]),
).toThrow("Duplicate redirect target for input 'test' in extension 'b'");
});
});
it('should set the correct attachment point for a redirect', () => {
const e1 = resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'nonexistent', input: 'nonexistent' },
inputs: {
test: createExtensionInput([coreExtensionData.reactElement], {
replaces: [{ id: 'replace', input: 'me' }],
}),
},
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
it('should set the correct attachment point for a redirect', () => {
const e1 = resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'nonexistent', input: 'nonexistent' },
inputs: {
test: createExtensionInput([coreExtensionData.reactElement], {
replaces: [{ id: 'replace', input: 'me' }],
}),
},
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
const e2 = resolveExtensionDefinition(
createExtension({
name: 'test-2',
attachTo: { id: 'replace', input: 'me' },
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
const e2 = resolveExtensionDefinition(
createExtension({
name: 'test-2',
attachTo: { id: 'replace', input: 'me' },
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
const tree = resolveAppTree('a', [
{ attachTo: e1.attachTo, id: 'a', extension: e1, disabled: false },
{ attachTo: e2.attachTo, id: 'b', extension: e2, disabled: false },
]);
const tree = resolveAppTree('a', [
{ attachTo: e1.attachTo, id: 'a', extension: e1, disabled: false },
{ attachTo: e2.attachTo, id: 'b', extension: e2, disabled: false },
]);
expect(tree.root).toMatchInlineSnapshot(`
expect(tree.root).toMatchInlineSnapshot(`
{
"attachments": {
"test": [
@@ -255,14 +255,62 @@ describe('buildAppTree', () => {
}
`);
expect(tree.orphans).toMatchInlineSnapshot(`[]`);
expect(tree.orphans).toMatchInlineSnapshot(`[]`);
expect(String(tree.root)).toMatchInlineSnapshot(`
expect(String(tree.root)).toMatchInlineSnapshot(`
"<a>
test [
<b />
]
</a>"
`);
});
it('should not allow redirects for attachment points that already exist', () => {
const e1 = resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'a', input: 'a' },
inputs: {
test: createExtensionInput([coreExtensionData.reactElement], {
replaces: [{ id: 'test-2', input: 'test' }],
}),
},
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
const e2 = resolveExtensionDefinition(
createExtension({
name: 'test-2',
attachTo: { id: 'b', input: 'b' },
inputs: {
test: createExtensionInput([coreExtensionData.reactElement]),
},
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
const e3 = resolveExtensionDefinition(
createExtension({
name: 'test-3',
attachTo: { id: 'test-2', input: 'test' },
output: [],
factory: () => [],
}),
) as Extension<unknown, unknown>;
const tree = resolveAppTree('test-2', [
{ attachTo: e1.attachTo, id: e1.id, extension: e1, disabled: false },
{ attachTo: e2.attachTo, id: e2.id, extension: e2, disabled: false },
{ attachTo: e3.attachTo, id: e3.id, extension: e3, disabled: false },
]);
expect(tree.nodes.get('test-3')?.edges.attachedTo?.node).toBe(
tree.nodes.get('test-2'),
);
});
});
});
@@ -92,6 +92,22 @@ function makeRedirectKey(attachTo: { id: string; input: string }) {
return `${attachTo.id}%${attachTo.input}`;
}
const MAX_REDIRECT_DEPTH = 100;
const isValidAttachmentPoint = (
attachTo: { id: string; input: string },
nodes: Map<string, SerializableAppNode>,
) => {
if (!nodes.has(attachTo.id)) {
return false;
}
return (
attachTo.input in
toInternalExtension(nodes.get(attachTo.id)!.spec.extension).inputs
);
};
/**
* Build the app tree by iterating through all node specs and constructing the app
* tree with all attachments in the same order as they appear in the input specs array.
@@ -143,8 +159,10 @@ export function resolveAppTree(
rootNode = node;
} else {
let attachTo = node.spec.attachTo;
while (redirectTargetsByKey.has(makeRedirectKey(attachTo))) {
attachTo = redirectTargetsByKey.get(makeRedirectKey(attachTo))!;
if (!isValidAttachmentPoint(attachTo, nodes)) {
attachTo =
redirectTargetsByKey.get(makeRedirectKey(attachTo)) ?? attachTo;
}
const parent = nodes.get(attachTo.id);
@@ -116,6 +116,7 @@ describe('ApiBlueprint', () => {
"extensionData": [
[Function],
],
"replaces": undefined,
},
},
"kind": "api",