feat: simplify the extension redirects
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user