frontend-plugin-api: support feature-level predicates
This lets plugin and module instances apply a shared condition to all of their extensions, while preserving extension-level conditions by combining them with logical AND during app spec resolution. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -532,4 +532,88 @@ describe('resolveAppNodeSpecs', () => {
|
||||
expect(specs).toHaveLength(1);
|
||||
expect(specs[0].if).toEqual(ifPredicate);
|
||||
});
|
||||
|
||||
it('should apply plugin if predicates to all plugin extensions', () => {
|
||||
const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' });
|
||||
const pluginIf = { featureFlags: { $contains: 'plugin-flag' } };
|
||||
const plugin = createFrontendPlugin({
|
||||
pluginId: 'test-plugin',
|
||||
if: pluginIf,
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'one',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: [dataRef],
|
||||
factory: () => [dataRef('one')],
|
||||
}),
|
||||
createExtension({
|
||||
name: 'two',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: [dataRef],
|
||||
factory: () => [dataRef('two')],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const specs = resolveAppNodeSpecs({
|
||||
features: [plugin],
|
||||
builtinExtensions: [],
|
||||
parameters: [],
|
||||
collector,
|
||||
});
|
||||
|
||||
expect(specs).toHaveLength(2);
|
||||
expect(specs[0].if).toEqual(pluginIf);
|
||||
expect(specs[1].if).toEqual(pluginIf);
|
||||
});
|
||||
|
||||
it('should merge plugin and module if predicates with extension predicates', () => {
|
||||
const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' });
|
||||
const pluginIf = { featureFlags: { $contains: 'plugin-flag' } };
|
||||
const moduleIf = { permissions: { $contains: 'module.permission' } };
|
||||
const extensionIf = { featureFlags: { $contains: 'extension-flag' } };
|
||||
const moduleExtensionIf = { featureFlags: { $contains: 'module-flag' } };
|
||||
const plugin = createFrontendPlugin({
|
||||
pluginId: 'test-plugin',
|
||||
if: pluginIf,
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'plugin-extension',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
if: extensionIf,
|
||||
output: [dataRef],
|
||||
factory: () => [dataRef('plugin')],
|
||||
}),
|
||||
createExtension({
|
||||
name: 'module-extension',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: [dataRef],
|
||||
factory: () => [dataRef('base')],
|
||||
}),
|
||||
],
|
||||
});
|
||||
const module = createFrontendModule({
|
||||
pluginId: 'test-plugin',
|
||||
if: moduleIf,
|
||||
extensions: [
|
||||
plugin.getExtension('test-plugin/module-extension').override({
|
||||
if: moduleExtensionIf,
|
||||
factory: () => [dataRef('module')],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const specs = resolveAppNodeSpecs({
|
||||
features: [plugin, module],
|
||||
builtinExtensions: [],
|
||||
parameters: [],
|
||||
collector,
|
||||
});
|
||||
|
||||
expect(specs).toHaveLength(2);
|
||||
expect(specs[0].id).toBe('test-plugin/plugin-extension');
|
||||
expect(specs[0].if).toEqual({ $all: [pluginIf, extensionIf] });
|
||||
expect(specs[1].id).toBe('test-plugin/module-extension');
|
||||
expect(specs[1].if).toEqual({ $all: [moduleIf, moduleExtensionIf] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
FrontendFeature,
|
||||
FrontendPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import { ExtensionParameters } from './readAppExtensionsConfig';
|
||||
import { AppNodeSpec } from '@backstage/frontend-plugin-api';
|
||||
import { OpaqueFrontendPlugin } from '@internal/frontend';
|
||||
@@ -40,6 +41,33 @@ function normalizePlugin(plugin: FrontendPlugin): FrontendPlugin {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
function combinePredicates(
|
||||
left: FilterPredicate | undefined,
|
||||
right: FilterPredicate | undefined,
|
||||
) {
|
||||
if (!left) {
|
||||
return right;
|
||||
}
|
||||
if (!right) {
|
||||
return left;
|
||||
}
|
||||
|
||||
return { $all: [left, right] };
|
||||
}
|
||||
|
||||
function getExtensionPredicate(options: {
|
||||
extension: Extension<any, any>;
|
||||
internalExtension: ReturnType<typeof toInternalExtension>;
|
||||
}) {
|
||||
if (options.extension.version === 'v2') {
|
||||
return options.extension.if;
|
||||
}
|
||||
if (options.internalExtension.version === 'v2') {
|
||||
return options.internalExtension.if;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function resolveAppNodeSpecs(options: {
|
||||
features?: FrontendFeature[];
|
||||
@@ -79,26 +107,41 @@ export function resolveAppNodeSpecs(options: {
|
||||
};
|
||||
|
||||
const pluginExtensions = plugins.flatMap(plugin => {
|
||||
return OpaqueFrontendPlugin.toInternal(plugin)
|
||||
.extensions.map(extension => ({
|
||||
const internalPlugin = OpaqueFrontendPlugin.toInternal(plugin);
|
||||
return internalPlugin.extensions
|
||||
.map(extension => ({
|
||||
...extension,
|
||||
plugin,
|
||||
if: combinePredicates(
|
||||
internalPlugin.if,
|
||||
extension.version === 'v2' ? extension.if : undefined,
|
||||
),
|
||||
}))
|
||||
.filter(filterForbidden);
|
||||
});
|
||||
const moduleExtensions = modules.flatMap(mod =>
|
||||
toInternalFrontendModule(mod)
|
||||
.extensions.flatMap(extension => {
|
||||
const moduleExtensions = modules.flatMap(mod => {
|
||||
const internalModule = toInternalFrontendModule(mod);
|
||||
return internalModule.extensions
|
||||
.flatMap(extension => {
|
||||
// Modules for plugins that are not installed are ignored
|
||||
const plugin = plugins.find(p => p.pluginId === mod.pluginId);
|
||||
if (!plugin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{ ...extension, plugin }];
|
||||
return [
|
||||
{
|
||||
...extension,
|
||||
plugin,
|
||||
if: combinePredicates(
|
||||
internalModule.if,
|
||||
extension.version === 'v2' ? extension.if : undefined,
|
||||
),
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(filterForbidden),
|
||||
);
|
||||
.filter(filterForbidden);
|
||||
});
|
||||
|
||||
const appPlugin =
|
||||
plugins.find(plugin => plugin.pluginId === 'app') ??
|
||||
@@ -116,10 +159,7 @@ export function resolveAppNodeSpecs(options: {
|
||||
source: plugin,
|
||||
attachTo: internalExtension.attachTo,
|
||||
disabled: internalExtension.disabled,
|
||||
if:
|
||||
internalExtension.version === 'v2'
|
||||
? internalExtension.if
|
||||
: undefined,
|
||||
if: getExtensionPredicate({ extension, internalExtension }),
|
||||
config: undefined as unknown,
|
||||
},
|
||||
};
|
||||
@@ -133,10 +173,7 @@ export function resolveAppNodeSpecs(options: {
|
||||
plugin: appPlugin,
|
||||
attachTo: internalExtension.attachTo,
|
||||
disabled: internalExtension.disabled,
|
||||
if:
|
||||
internalExtension.version === 'v2'
|
||||
? internalExtension.if
|
||||
: undefined,
|
||||
if: getExtensionPredicate({ extension, internalExtension }),
|
||||
config: undefined as unknown,
|
||||
},
|
||||
};
|
||||
@@ -156,8 +193,10 @@ export function resolveAppNodeSpecs(options: {
|
||||
configuredExtensions[index].extension = internalExtension;
|
||||
configuredExtensions[index].params.attachTo = internalExtension.attachTo;
|
||||
configuredExtensions[index].params.disabled = internalExtension.disabled;
|
||||
configuredExtensions[index].params.if =
|
||||
internalExtension.version === 'v2' ? internalExtension.if : undefined;
|
||||
configuredExtensions[index].params.if = getExtensionPredicate({
|
||||
extension,
|
||||
internalExtension,
|
||||
});
|
||||
} else {
|
||||
// Add the extension as a new one when not overriding an existing one
|
||||
configuredExtensions.push({
|
||||
@@ -167,10 +206,7 @@ export function resolveAppNodeSpecs(options: {
|
||||
source: extension.plugin,
|
||||
attachTo: internalExtension.attachTo,
|
||||
disabled: internalExtension.disabled,
|
||||
if:
|
||||
internalExtension.version === 'v2'
|
||||
? internalExtension.if
|
||||
: undefined,
|
||||
if: getExtensionPredicate({ extension, internalExtension }),
|
||||
config: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user