Merge pull request #32496 from backstage/rugvip/internal
frontend-plugin-api: add new internal extension input option, complete app-react deprecations
This commit is contained in:
@@ -59,6 +59,14 @@ export type AppErrorTypes = {
|
||||
inputName: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_INPUT_INTERNAL_IGNORED: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
extensionId: string;
|
||||
plugin: FrontendPlugin;
|
||||
};
|
||||
};
|
||||
EXTENSION_ATTACHMENT_CONFLICT: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
|
||||
@@ -142,6 +142,36 @@ function createV1Extension(opts: {
|
||||
return ext;
|
||||
}
|
||||
|
||||
function mirrorInputs(ctx: {
|
||||
inputs: {
|
||||
[name in string]:
|
||||
| undefined
|
||||
| ResolvedExtensionInput<ExtensionInput>
|
||||
| Array<ResolvedExtensionInput<ExtensionInput>>;
|
||||
};
|
||||
}) {
|
||||
return [
|
||||
inputMirrorDataRef(
|
||||
Object.fromEntries(
|
||||
Object.entries(ctx.inputs).map(([k, v]) => [
|
||||
k,
|
||||
Array.isArray(v)
|
||||
? v.map(vi => ({
|
||||
node: vi.node,
|
||||
test: vi.get(testDataRef),
|
||||
other: vi.get(otherDataRef),
|
||||
}))
|
||||
: {
|
||||
node: v?.node,
|
||||
test: v?.get(testDataRef),
|
||||
other: v?.get(otherDataRef),
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
describe('instantiateAppNodeTree', () => {
|
||||
describe('v1', () => {
|
||||
const simpleExtension = createV1Extension({
|
||||
@@ -237,6 +267,60 @@ describe('instantiateAppNodeTree', () => {
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
});
|
||||
|
||||
it('should ignore non-matching plugin attachments for internal inputs', () => {
|
||||
const otherPlugin = createFrontendPlugin({ pluginId: 'other' });
|
||||
const tree = resolveAppTree(
|
||||
'root-node',
|
||||
[
|
||||
makeSpec(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput([testDataRef], {
|
||||
singleton: true,
|
||||
internal: true,
|
||||
}),
|
||||
},
|
||||
output: [inputMirrorDataRef],
|
||||
factory: mirrorInputs,
|
||||
}),
|
||||
{ namespace: 'root-node' },
|
||||
),
|
||||
),
|
||||
makeSpec(simpleExtension, {
|
||||
id: 'child-node-app',
|
||||
attachTo: { id: 'root-node', input: 'test' },
|
||||
}),
|
||||
makeSpec(simpleExtension, {
|
||||
id: 'child-node-other',
|
||||
attachTo: { id: 'root-node', input: 'test' },
|
||||
plugin: otherPlugin,
|
||||
}),
|
||||
],
|
||||
collector,
|
||||
);
|
||||
|
||||
instantiateAppNodeTree(tree.root, testApis, collector);
|
||||
|
||||
expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({
|
||||
test: { node: { spec: { id: 'child-node-app' } }, test: 'test' },
|
||||
});
|
||||
expect(collector.collectErrors()).toEqual([
|
||||
{
|
||||
code: 'EXTENSION_INPUT_INTERNAL_IGNORED',
|
||||
message:
|
||||
"extension 'child-node-other' from plugin 'other' attached to input 'test' on 'root-node' was ignored, the input is marked as internal and attached extensions must therefore be provided via an override or a module for the 'app' plugin, not the 'other' plugin",
|
||||
context: {
|
||||
node: tree.root,
|
||||
inputName: 'test',
|
||||
extensionId: 'child-node-other',
|
||||
plugin: otherPlugin,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not instantiate disabled attachments', () => {
|
||||
const tree = resolveAppTree(
|
||||
'root-node',
|
||||
@@ -783,36 +867,6 @@ describe('instantiateAppNodeTree', () => {
|
||||
{ namespace: 'app' },
|
||||
);
|
||||
|
||||
function mirrorInputs(ctx: {
|
||||
inputs: {
|
||||
[name in string]:
|
||||
| undefined
|
||||
| ResolvedExtensionInput<ExtensionInput>
|
||||
| Array<ResolvedExtensionInput<ExtensionInput>>;
|
||||
};
|
||||
}) {
|
||||
return [
|
||||
inputMirrorDataRef(
|
||||
Object.fromEntries(
|
||||
Object.entries(ctx.inputs).map(([k, v]) => [
|
||||
k,
|
||||
Array.isArray(v)
|
||||
? v.map(vi => ({
|
||||
node: vi.node,
|
||||
test: vi.get(testDataRef),
|
||||
other: vi.get(otherDataRef),
|
||||
}))
|
||||
: {
|
||||
node: v?.node,
|
||||
test: v?.get(testDataRef),
|
||||
other: v?.get(otherDataRef),
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
it('should instantiate a single node', () => {
|
||||
const tree = resolveAppTree(
|
||||
'root-node',
|
||||
|
||||
@@ -247,10 +247,32 @@ function resolveV2Inputs(
|
||||
inputMap: { [inputName in string]: ExtensionInput },
|
||||
attachments: ReadonlyMap<string, AppNode[]>,
|
||||
parentCollector: ErrorCollector<{ node: AppNode }>,
|
||||
node: AppNode,
|
||||
): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }> {
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedNodes = attachments.get(inputName) ?? [];
|
||||
const allAttachedNodes = attachments.get(inputName) ?? [];
|
||||
const collector = parentCollector.child({ inputName });
|
||||
const inputPluginId = node.spec.plugin.id;
|
||||
|
||||
const attachedNodes = input.config.internal
|
||||
? allAttachedNodes.filter(attachment => {
|
||||
const attachmentPluginId = attachment.spec.plugin.id;
|
||||
if (attachmentPluginId !== inputPluginId) {
|
||||
collector.report({
|
||||
code: 'EXTENSION_INPUT_INTERNAL_IGNORED',
|
||||
message:
|
||||
`extension '${attachment.spec.id}' from plugin '${attachmentPluginId}' attached to input '${inputName}' on '${node.spec.id}' was ignored, ` +
|
||||
`the input is marked as internal and attached extensions must therefore be provided via an override or a module for the '${inputPluginId}' plugin, not the '${attachmentPluginId}' plugin`,
|
||||
context: {
|
||||
extensionId: attachment.spec.id,
|
||||
plugin: attachment.spec.plugin,
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
: allAttachedNodes;
|
||||
|
||||
if (input.config.singleton) {
|
||||
if (attachedNodes.length > 1) {
|
||||
@@ -371,6 +393,7 @@ export function createAppNodeInstance(options: {
|
||||
internalExtension.inputs,
|
||||
attachments,
|
||||
collector,
|
||||
node,
|
||||
),
|
||||
};
|
||||
const outputDataValues = options.extensionFactoryMiddleware
|
||||
|
||||
@@ -38,6 +38,14 @@ export type AppErrorTypes = {
|
||||
EXTENSION_INPUT_DATA_MISSING: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
EXTENSION_INPUT_INTERNAL_IGNORED: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
extensionId: string;
|
||||
plugin: FrontendPlugin;
|
||||
};
|
||||
};
|
||||
EXTENSION_ATTACHMENT_CONFLICT: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user