diff --git a/.changeset/ff-perm-enabled-frontend-app-api.md b/.changeset/ff-perm-enabled-frontend-app-api.md
new file mode 100644
index 0000000000..dc3bba03ba
--- /dev/null
+++ b/.changeset/ff-perm-enabled-frontend-app-api.md
@@ -0,0 +1,7 @@
+---
+'@backstage/frontend-app-api': patch
+---
+
+Extensions with an `enabled` predicate are now evaluated before app tree instantiation, so pages that do not meet their conditions are excluded from the router tree.
+
+`prepareSpecializedApp().finalize()` is now async. Extensions whose `enabled` predicate references `permissions` are checked against the current user's allowed permissions (via a single batched call to `permissionApiRef`) before the app tree is instantiated.
diff --git a/.changeset/ff-perm-enabled-frontend-defaults.md b/.changeset/ff-perm-enabled-frontend-defaults.md
new file mode 100644
index 0000000000..c811cad8eb
--- /dev/null
+++ b/.changeset/ff-perm-enabled-frontend-defaults.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-defaults': patch
+---
+
+Updated `createApp` to await the now-async `finalize()` call from `prepareSpecializedApp`, enabling permission-gated extensions to be resolved before the app tree is rendered.
diff --git a/.changeset/ff-perm-enabled-frontend-plugin-api.md b/.changeset/ff-perm-enabled-frontend-plugin-api.md
new file mode 100644
index 0000000000..67fd93eb19
--- /dev/null
+++ b/.changeset/ff-perm-enabled-frontend-plugin-api.md
@@ -0,0 +1,7 @@
+---
+'@backstage/frontend-plugin-api': patch
+---
+
+Added `enabled` option to `createExtension`, `createExtensionBlueprint`, and `AppNodeSpec`, accepting a `FilterPredicate` from `@backstage/filter-predicates` to conditionally enable extensions based on feature flags or other runtime conditions.
+
+Added `permissions` option to `createFrontendPlugin` and `createFrontendModule`, allowing plugins and modules to declare which permissions they use. These permissions are checked at app startup so that extensions can conditionally enable themselves using a `permissions` predicate, e.g. `enabled: { permissions: { $contains: 'catalog.entity.create' } }`.
diff --git a/packages/app/src/examples/pagesPlugin.tsx b/packages/app/src/examples/pagesPlugin.tsx
index 35426b589b..374935e843 100644
--- a/packages/app/src/examples/pagesPlugin.tsx
+++ b/packages/app/src/examples/pagesPlugin.tsx
@@ -65,7 +65,8 @@ const IndexPage = PageBlueprint.make({
const page1Link = useRouteRef(page1RouteRef);
return (
- op
+
Example Pages Plugin
+
Navigation
{page1Link && (
Page 1
@@ -83,6 +84,47 @@ const IndexPage = PageBlueprint.make({
Settings
+
+
Permission Enablement Examples
+
+ The following pages demonstrate conditional extension enablement
+ via the enabled predicate using permissions. They
+ will only appear when the user has the required permissions.
+
+
+
+
+ Permission Gated Example
+ {' '}
+ — requires catalog.entity.create
+
+
+
+
Feature Flag Enablement Examples
+
+ The following pages demonstrate conditional extension enablement
+ via the enabled predicate. They will only appear in
+ the router tree when their conditions are satisfied. Toggle the
+ relevant feature flags in Settings,
+ then refresh the app to see the pages appear.
+
+
+
+ Feature Flag Example —
+ requires the experimental-features flag
+
+
+ All Flags Example —
+ requires both experimental-features and{' '}
+ advanced-features ($all)
+
+
+ Any Flag Example — requires{' '}
+ either experimental-features or{' '}
+ beta-access ($any)
+
+
+
);
@@ -150,6 +192,155 @@ const ExternalPage = PageBlueprint.make({
},
});
+// Example: Page enabled only when a single feature flag is active.
+//
+// The `enabled` predicate is evaluated once at app startup (before the router
+// tree is built), so this page simply won't exist in the app until the flag is
+// toggled and the page is refreshed.
+//
+// To test: enable the 'experimental-features' flag in Settings, then refresh.
+const FeatureFlagPage = PageBlueprint.make({
+ name: 'featureFlagExample',
+ params: {
+ path: '/feature-flag-example',
+ loader: async () => {
+ const Component = () => {
+ const indexLink = useRouteRef(indexRouteRef);
+ return (
+
+
Feature Flag Enabled Page
+
+ This page is only present in the app when the{' '}
+ experimental-features feature flag is active.
+
+
+ It uses a simple{' '}
+
+ {'{ featureFlags: { $contains: "experimental-features" } }'}
+ {' '}
+ predicate.
+
+ {indexLink &&
Go back}
+
+ );
+ };
+ return
;
+ },
+ },
+ enabled: { featureFlags: { $contains: 'experimental-features' } },
+});
+
+// Example: Page enabled only when ALL of several feature flags are active.
+//
+// The $all operator requires every nested predicate to be satisfied. This page
+// won't appear unless both 'experimental-features' and 'advanced-features' are
+// enabled at the same time.
+//
+// To test: enable BOTH flags in Settings, then refresh.
+const AllFlagsPage = PageBlueprint.make({
+ name: 'allFlagsExample',
+ params: {
+ path: '/all-flags-example',
+ loader: async () => {
+ const Component = () => {
+ const indexLink = useRouteRef(indexRouteRef);
+ return (
+
+
All Flags Required Page
+
+ This page requires both {' '}
+ experimental-features and{' '}
+ advanced-features to be active simultaneously.
+
+
+ It uses a $all predicate to AND the two conditions
+ together.
+
+ {indexLink &&
Go back}
+
+ );
+ };
+ return
;
+ },
+ },
+ enabled: {
+ $all: [
+ { featureFlags: { $contains: 'experimental-features' } },
+ { featureFlags: { $contains: 'advanced-features' } },
+ ],
+ },
+});
+
+// Example: Page enabled when ANY one of several feature flags is active.
+//
+// The $any operator is satisfied as soon as at least one nested predicate
+// matches. Enabling either 'experimental-features' or 'beta-access' will make
+// this page appear.
+//
+// To test: enable at least one of the two flags in Settings, then refresh.
+const AnyFlagPage = PageBlueprint.make({
+ name: 'anyFlagExample',
+ params: {
+ path: '/any-flag-example',
+ loader: async () => {
+ const Component = () => {
+ const indexLink = useRouteRef(indexRouteRef);
+ return (
+
+
Any Flag Sufficient Page
+
+ This page appears when either {' '}
+ experimental-features or beta-access is
+ active.
+
+
+ It uses a $any predicate to OR the two conditions
+ together.
+
+ {indexLink &&
Go back}
+
+ );
+ };
+ return
;
+ },
+ },
+ enabled: {
+ $any: [
+ { featureFlags: { $contains: 'experimental-features' } },
+ { featureFlags: { $contains: 'beta-access' } },
+ ],
+ },
+});
+
+// Example: Page enabled only when the user is allowed to create catalog entities.
+//
+// The `enabled` predicate is evaluated once at app startup (after sign-in),
+// so this page simply won't exist in the router tree if the user lacks the
+// required permission.
+const PermissionGatedPage = PageBlueprint.make({
+ name: 'permissionGatedExample',
+ params: {
+ path: '/permission-gated-example',
+ loader: async () => {
+ const Component = () => {
+ const indexLink = useRouteRef(indexRouteRef);
+ return (
+
+
Permission Gated Page
+
+ This page is only present when the user has the{' '}
+ catalog.entity.create permission.
+
+ {indexLink &&
Go back}
+
+ );
+ };
+ return
;
+ },
+ },
+ enabled: { permissions: { $contains: 'catalog.entity.create' } },
+});
+
export const pagesPlugin = createFrontendPlugin({
pluginId: 'pages',
// routes: {
@@ -170,5 +361,18 @@ export const pagesPlugin = createFrontendPlugin({
externalRoutes: {
pageX: externalPageXRouteRef,
},
- extensions: [IndexPage, Page1, ExternalPage],
+ featureFlags: [
+ { name: 'experimental-features' },
+ { name: 'advanced-features' },
+ { name: 'beta-access' },
+ ],
+ extensions: [
+ IndexPage,
+ Page1,
+ ExternalPage,
+ FeatureFlagPage,
+ AllFlagsPage,
+ AnyFlagPage,
+ PermissionGatedPage,
+ ],
});
diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json
index 1951c18828..b188f94348 100644
--- a/packages/frontend-app-api/package.json
+++ b/packages/frontend-app-api/package.json
@@ -36,8 +36,10 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/filter-predicates": "workspace:^",
"@backstage/frontend-defaults": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"lodash": "^4.17.21",
diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts
index e52bbfa320..ac22c34696 100644
--- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts
+++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts
@@ -1782,4 +1782,96 @@ describe('instantiateAppNodeTree', () => {
});
});
});
+
+ describe('enabled predicate', () => {
+ function makeNodeWithEnabled(
+ enabled: AppNodeSpec['enabled'],
+ disabled = false,
+ ): AppNode {
+ const ext = resolveExtensionDefinition(
+ createExtension({
+ attachTo: { id: 'ignored', input: 'ignored' },
+ output: [testDataRef],
+ factory: () => [testDataRef('value')],
+ }),
+ { namespace: 'test-ext' },
+ );
+ return {
+ spec: {
+ id: ext.id,
+ attachTo: ext.attachTo,
+ disabled,
+ enabled,
+ extension: ext as Extension
,
+ plugin: createFrontendPlugin({ pluginId: 'app' }),
+ },
+ edges: { attachments: new Map() },
+ };
+ }
+
+ it('should skip a node when the predicate is not satisfied', () => {
+ const node = makeNodeWithEnabled({
+ featureFlags: { $contains: 'the-flag' },
+ });
+ const tree = resolveAppTree('test-ext', [node.spec], collector);
+ instantiateAppNodeTree(tree.root, testApis, collector, undefined, {
+ featureFlags: [],
+ });
+ expect(tree.root.instance).toBeUndefined();
+ });
+
+ it('should instantiate a node when the predicate is satisfied', () => {
+ const node = makeNodeWithEnabled({
+ featureFlags: { $contains: 'the-flag' },
+ });
+ const tree = resolveAppTree('test-ext', [node.spec], collector);
+ instantiateAppNodeTree(tree.root, testApis, collector, undefined, {
+ featureFlags: ['the-flag'],
+ });
+ expect(tree.root.instance).toBeDefined();
+ expect(tree.root.instance?.getData(testDataRef)).toBe('value');
+ });
+
+ it('should support $all operator across multiple flags', () => {
+ const node = makeNodeWithEnabled({
+ $all: [
+ { featureFlags: { $contains: 'flag-a' } },
+ { featureFlags: { $contains: 'flag-b' } },
+ ],
+ });
+ const tree = resolveAppTree('test-ext', [node.spec], collector);
+
+ // Only one flag active — should not instantiate
+ instantiateAppNodeTree(tree.root, testApis, collector, undefined, {
+ featureFlags: ['flag-a'],
+ });
+ expect(tree.root.instance).toBeUndefined();
+
+ // Both flags active — should instantiate
+ const tree2 = resolveAppTree('test-ext', [node.spec], collector);
+ instantiateAppNodeTree(tree2.root, testApis, collector, undefined, {
+ featureFlags: ['flag-a', 'flag-b'],
+ });
+ expect(tree2.root.instance).toBeDefined();
+ });
+
+ it('should instantiate nodes without an enabled field regardless of predicateContext', () => {
+ const node = makeNodeWithEnabled(undefined);
+ const tree = resolveAppTree('test-ext', [node.spec], collector);
+ instantiateAppNodeTree(tree.root, testApis, collector, undefined, {
+ featureFlags: [],
+ });
+ expect(tree.root.instance).toBeDefined();
+ });
+
+ it('should instantiate nodes with enabled predicate when predicateContext is not provided', () => {
+ const node = makeNodeWithEnabled({
+ featureFlags: { $contains: 'the-flag' },
+ });
+ const tree = resolveAppTree('test-ext', [node.spec], collector);
+ // No predicateContext passed — predicate evaluation is skipped
+ instantiateAppNodeTree(tree.root, testApis, collector);
+ expect(tree.root.instance).toBeDefined();
+ });
+ });
});
diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts
index 85a7baa11c..d60a910487 100644
--- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts
+++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts
@@ -29,6 +29,7 @@ import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api';
import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
import { createExtensionDataContainer } from '@internal/frontend';
import { ErrorCollector } from '../wiring/createErrorCollector';
+import { evaluateFilterPredicate } from '@backstage/filter-predicates';
const INSTANTIATION_FAILED = new Error('Instantiation failed');
@@ -509,10 +510,25 @@ export function instantiateAppNodeTree(
apis: ApiHolder,
collector: ErrorCollector,
extensionFactoryMiddleware?: ExtensionFactoryMiddleware,
- options?: {
- stopAtAttachment?(ctx: { node: AppNode; input: string }): boolean;
- },
+ optionsOrPredicateContext?:
+ | {
+ stopAtAttachment?(ctx: { node: AppNode; input: string }): boolean;
+ predicateContext?: Record;
+ }
+ | Record,
): boolean {
+ const options: {
+ stopAtAttachment?(ctx: { node: AppNode; input: string }): boolean;
+ predicateContext?: Record;
+ } =
+ optionsOrPredicateContext &&
+ ('stopAtAttachment' in optionsOrPredicateContext ||
+ 'predicateContext' in optionsOrPredicateContext)
+ ? optionsOrPredicateContext
+ : {
+ predicateContext: optionsOrPredicateContext,
+ };
+
function createInstance(node: AppNode): AppNodeInstance | undefined {
if (node.instance) {
return node.instance;
@@ -520,6 +536,13 @@ export function instantiateAppNodeTree(
if (node.spec.disabled) {
return undefined;
}
+ if (
+ options?.predicateContext !== undefined &&
+ node.spec.enabled !== undefined &&
+ !evaluateFilterPredicate(node.spec.enabled, options.predicateContext)
+ ) {
+ return undefined;
+ }
const instantiatedAttachments = new Map();
diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts
index 3ccdf5258f..279ae13af1 100644
--- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts
+++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts
@@ -15,6 +15,8 @@
*/
import {
+ createExtension,
+ createExtensionDataRef,
createFrontendModule,
createFrontendPlugin,
Extension,
@@ -506,4 +508,28 @@ describe('resolveAppNodeSpecs', () => {
},
]);
});
+
+ it('should carry enabled predicate through to AppNodeSpec', () => {
+ const dataRef = createExtensionDataRef().with({ id: 'test.data' });
+ const enabledPredicate = { featureFlags: { $contains: 'my-flag' } };
+ const plugin = createFrontendPlugin({
+ pluginId: 'test-plugin',
+ extensions: [
+ createExtension({
+ attachTo: { id: 'app', input: 'root' },
+ enabled: enabledPredicate,
+ output: [dataRef],
+ factory: () => [dataRef('value')],
+ }),
+ ],
+ });
+ const specs = resolveAppNodeSpecs({
+ features: [plugin],
+ builtinExtensions: [],
+ parameters: [],
+ collector,
+ });
+ expect(specs).toHaveLength(1);
+ expect(specs[0].enabled).toEqual(enabledPredicate);
+ });
});
diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts
index a095acee0e..005e6a05b9 100644
--- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts
+++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts
@@ -116,6 +116,10 @@ export function resolveAppNodeSpecs(options: {
source: plugin,
attachTo: internalExtension.attachTo,
disabled: internalExtension.disabled,
+ enabled:
+ internalExtension.version === 'v2'
+ ? internalExtension.enabled
+ : undefined,
config: undefined as unknown,
},
};
@@ -129,6 +133,10 @@ export function resolveAppNodeSpecs(options: {
plugin: appPlugin,
attachTo: internalExtension.attachTo,
disabled: internalExtension.disabled,
+ enabled:
+ internalExtension.version === 'v2'
+ ? internalExtension.enabled
+ : undefined,
config: undefined as unknown,
},
};
@@ -148,6 +156,10 @@ export function resolveAppNodeSpecs(options: {
configuredExtensions[index].extension = internalExtension;
configuredExtensions[index].params.attachTo = internalExtension.attachTo;
configuredExtensions[index].params.disabled = internalExtension.disabled;
+ configuredExtensions[index].params.enabled =
+ internalExtension.version === 'v2'
+ ? internalExtension.enabled
+ : undefined;
} else {
// Add the extension as a new one when not overriding an existing one
configuredExtensions.push({
@@ -157,6 +169,10 @@ export function resolveAppNodeSpecs(options: {
source: extension.plugin,
attachTo: internalExtension.attachTo,
disabled: internalExtension.disabled,
+ enabled:
+ internalExtension.version === 'v2'
+ ? internalExtension.enabled
+ : undefined,
config: undefined,
},
});
@@ -235,6 +251,7 @@ export function resolveAppNodeSpecs(options: {
attachTo: param.params.attachTo,
extension: param.extension,
disabled: param.params.disabled,
+ enabled: param.params.enabled,
plugin: param.params.plugin,
source: param.params.source,
config: param.params.config,
diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md
index cfe4f55b2d..bc620c95a3 100644
--- a/packages/frontend-defaults/report.api.md
+++ b/packages/frontend-defaults/report.api.md
@@ -23,6 +23,7 @@ export function createApp(options?: CreateAppOptions): {
// @public
export interface CreateAppOptions {
advanced?: {
+ allowUnknownExtensionConfig?: boolean;
configLoader?: () => Promise<{
config: ConfigApi;
}>;
diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx
index c4cf43fb2e..2c1aa0519c 100644
--- a/packages/frontend-defaults/src/createApp.tsx
+++ b/packages/frontend-defaults/src/createApp.tsx
@@ -156,22 +156,33 @@ function PreparedAppRoot(props: {
}): JSX.Element {
const signIn = props.preparedApp.getSignIn();
const [finalizeError, setFinalizeError] = useState();
- const [finalizedApp, setFinalizedApp] = useState(() => {
- if (!signIn) {
- return props.preparedApp.finalize();
- }
- return undefined;
- });
+ const [finalizedApp, setFinalizedApp] = useState<
+ ReturnType | undefined
+ >(undefined);
useEffect(() => {
let cancelled = false;
+ const runFinalize = async () => {
+ try {
+ const predicateContext = await props.preparedApp.buildPredicateContext();
+ if (cancelled) {
+ return;
+ }
+ setFinalizedApp(props.preparedApp.finalize(predicateContext));
+ } catch (error) {
+ if (cancelled) {
+ return;
+ }
+ setFinalizeError(error as Error);
+ }
+ };
if (signIn) {
void signIn.complete
- .then(async () => {
+ .then(() => {
if (cancelled) {
return;
}
- setFinalizedApp(props.preparedApp.finalize());
+ void runFinalize();
})
.catch(error => {
if (cancelled) {
@@ -179,8 +190,9 @@ function PreparedAppRoot(props: {
}
setFinalizeError(error);
});
+ } else {
+ void runFinalize();
}
-
return () => {
cancelled = true;
};
@@ -191,7 +203,7 @@ function PreparedAppRoot(props: {
}
if (!finalizedApp) {
- return signIn!.element;
+ return signIn?.element ?? <>>;
}
return renderFinalizedApp(finalizedApp);
diff --git a/packages/frontend-internal/package.json b/packages/frontend-internal/package.json
index 52cdb33641..abfac8a51a 100644
--- a/packages/frontend-internal/package.json
+++ b/packages/frontend-internal/package.json
@@ -23,6 +23,7 @@
"test": "backstage-cli package test"
},
"dependencies": {
+ "@backstage/filter-predicates": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^"
diff --git a/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts b/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts
index d6a4eb2eab..a3f1ae8ff3 100644
--- a/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts
+++ b/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts
@@ -28,6 +28,7 @@ import {
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { ResolvedExtensionInputs } from '../../../frontend-plugin-api/src/wiring/createExtension';
import { OpaqueType } from '@internal/opaque';
+import { FilterPredicate } from '@backstage/filter-predicates';
export const OpaqueExtensionDefinition = OpaqueType.create<{
public: OverridableExtensionDefinition;
@@ -70,6 +71,7 @@ export const OpaqueExtensionDefinition = OpaqueType.create<{
readonly name?: string;
readonly attachTo: ExtensionDefinitionAttachTo;
readonly disabled: boolean;
+ readonly enabled?: FilterPredicate;
readonly configSchema?: PortableSchema;
readonly inputs: { [inputName in string]: ExtensionInput };
readonly output: Array;
diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json
index 568d542a38..66973d4406 100644
--- a/packages/frontend-plugin-api/package.json
+++ b/packages/frontend-plugin-api/package.json
@@ -45,6 +45,7 @@
},
"dependencies": {
"@backstage/errors": "workspace:^",
+ "@backstage/filter-predicates": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"zod": "^3.25.76",
diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md
index 74823c0639..0af67779e7 100644
--- a/packages/frontend-plugin-api/report.api.md
+++ b/packages/frontend-plugin-api/report.api.md
@@ -14,6 +14,7 @@ import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-
import { ExtensionBlueprintParams as ExtensionBlueprintParams_2 } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef as ExtensionDataRef_2 } from '@backstage/frontend-plugin-api';
import { ExtensionInput as ExtensionInput_2 } from '@backstage/frontend-plugin-api';
+import { FilterPredicate } from '@backstage/filter-predicates';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { JSX as JSX_2 } from 'react';
@@ -254,6 +255,8 @@ export interface AppNodeSpec {
// (undocumented)
readonly disabled: boolean;
// (undocumented)
+ readonly enabled?: FilterPredicate;
+ // (undocumented)
readonly extension: Extension;
// (undocumented)
readonly id: string;
@@ -577,6 +580,7 @@ export type CreateExtensionBlueprintOptions<
attachTo: ExtensionDefinitionAttachTo &
VerifyExtensionAttachTo;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TInputs;
output: Array;
config?: {
@@ -663,6 +667,7 @@ export type CreateExtensionOptions<
attachTo: ExtensionDefinitionAttachTo &
VerifyExtensionAttachTo;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TInputs;
output: Array;
config?: {
@@ -1026,6 +1031,7 @@ export interface ExtensionBlueprint<
attachTo?: ExtensionDefinitionAttachTo &
VerifyExtensionAttachTo, UParentInputs>;
disabled?: boolean;
+ enabled?: FilterPredicate;
params: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
@@ -1061,6 +1067,7 @@ export interface ExtensionBlueprint<
UParentInputs
>;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
@@ -1702,6 +1709,7 @@ export interface OverridableExtensionDefinition<
UParentInputs
>;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts
index 16369680f2..c6c367e0b5 100644
--- a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts
+++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts
@@ -17,6 +17,7 @@
import { createApiRef } from '../system';
import { FrontendPlugin, Extension, ExtensionDataRef } from '../../wiring';
import { ExtensionAttachTo } from '../../wiring/resolveExtensionDefinition';
+import { FilterPredicate } from '@backstage/filter-predicates';
/**
* The specification for this {@link AppNode} in the {@link AppTree}.
@@ -32,6 +33,7 @@ export interface AppNodeSpec {
readonly attachTo: ExtensionAttachTo;
readonly extension: Extension;
readonly disabled: boolean;
+ readonly enabled?: FilterPredicate;
readonly config?: unknown;
readonly plugin: FrontendPlugin;
}
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts
index 0a1e9c91f8..10ca58d0da 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtension.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts
@@ -36,6 +36,7 @@ import {
} from './createExtensionBlueprint';
import { FrontendPlugin } from './createFrontendPlugin';
import { FrontendModule } from './createFrontendModule';
+import { FilterPredicate } from '@backstage/filter-predicates';
/**
* This symbol is used to pass parameter overrides from the extension override to the blueprint factory
@@ -174,6 +175,7 @@ export type CreateExtensionOptions<
attachTo: ExtensionDefinitionAttachTo &
VerifyExtensionAttachTo;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TInputs;
output: Array;
config?: {
@@ -255,6 +257,7 @@ export interface OverridableExtensionDefinition<
UParentInputs
>;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
@@ -474,6 +477,7 @@ export function createExtension<
name: options.name,
attachTo: options.attachTo,
disabled: options.disabled ?? false,
+ enabled: options.enabled,
inputs: bindInputs(options.inputs, options.kind, options.name),
output: options.output,
configSchema,
@@ -551,6 +555,7 @@ export function createExtension<
attachTo: (overrideOptions.attachTo ??
options.attachTo) as ExtensionDefinitionAttachTo,
disabled: overrideOptions.disabled ?? options.disabled,
+ enabled: overrideOptions.enabled ?? options.enabled,
inputs: bindInputs(
{
...(options.inputs ?? {}),
diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts
index a7537d27e9..538a47003b 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts
@@ -36,6 +36,7 @@ import {
} from './resolveInputOverrides';
import { ExtensionDataContainer } from './types';
import { PageBlueprint } from '../blueprints/PageBlueprint';
+import { FilterPredicate } from '@backstage/filter-predicates';
/**
* A function used to define a parameter mapping function in order to facilitate
@@ -114,6 +115,7 @@ export type CreateExtensionBlueprintOptions<
attachTo: ExtensionDefinitionAttachTo &
VerifyExtensionAttachTo;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TInputs;
output: Array;
config?: {
@@ -221,6 +223,7 @@ export interface ExtensionBlueprint<
attachTo?: ExtensionDefinitionAttachTo &
VerifyExtensionAttachTo, UParentInputs>;
disabled?: boolean;
+ enabled?: FilterPredicate;
params: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
@@ -261,6 +264,7 @@ export interface ExtensionBlueprint<
UParentInputs
>;
disabled?: boolean;
+ enabled?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
@@ -510,6 +514,7 @@ export function createExtensionBlueprint<
attachTo: (args.attachTo ??
options.attachTo) as ExtensionDefinitionAttachTo,
disabled: args.disabled ?? options.disabled,
+ enabled: args.enabled ?? options.enabled,
inputs: options.inputs,
output: options.output as ExtensionDataRef[],
config: options.config,
@@ -527,6 +532,7 @@ export function createExtensionBlueprint<
attachTo: (args.attachTo ??
options.attachTo) as ExtensionDefinitionAttachTo,
disabled: args.disabled ?? options.disabled,
+ enabled: args.enabled ?? options.enabled,
inputs: { ...args.inputs, ...options.inputs },
output: (args.output ?? options.output) as ExtensionDataRef[],
config:
diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
index 70a0bd7837..9476c48256 100644
--- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
+++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
@@ -28,6 +28,7 @@ import {
OpaqueExtensionDefinition,
OpaqueExtensionInput,
} from '@internal/frontend';
+import { FilterPredicate } from '@backstage/filter-predicates';
/** @public */
export type ExtensionAttachTo = { id: string; input: string };
@@ -74,6 +75,7 @@ export type InternalExtension = Extension<
}
| {
readonly version: 'v2';
+ readonly enabled?: FilterPredicate;
readonly inputs: { [inputName in string]: ExtensionInput };
readonly output: Array;
factory(options: {
diff --git a/plugins/permission-react/src/apis/IdentityPermissionApi.ts b/plugins/permission-react/src/apis/IdentityPermissionApi.ts
index dc1fc3b5a3..7a3ac6482e 100644
--- a/plugins/permission-react/src/apis/IdentityPermissionApi.ts
+++ b/plugins/permission-react/src/apis/IdentityPermissionApi.ts
@@ -52,11 +52,18 @@ export class IdentityPermissionApi implements PermissionApi {
async authorize(
request: AuthorizePermissionRequest,
- ): Promise {
- const response = await this.permissionClient.authorize(
- [request],
+ ): Promise;
+ async authorize(
+ requests: AuthorizePermissionRequest[],
+ ): Promise;
+ async authorize(
+ request: AuthorizePermissionRequest | AuthorizePermissionRequest[],
+ ): Promise {
+ const requests = Array.isArray(request) ? request : [request];
+ const responses = await this.permissionClient.authorize(
+ requests,
await this.identityApi.getCredentials(),
);
- return response[0];
+ return Array.isArray(request) ? responses : responses[0];
}
}
diff --git a/plugins/permission-react/src/apis/PermissionApi.ts b/plugins/permission-react/src/apis/PermissionApi.ts
index d3a37421ef..5f538a068d 100644
--- a/plugins/permission-react/src/apis/PermissionApi.ts
+++ b/plugins/permission-react/src/apis/PermissionApi.ts
@@ -30,6 +30,9 @@ export type PermissionApi = {
authorize(
request: EvaluatePermissionRequest,
): Promise;
+ authorize(
+ requests: EvaluatePermissionRequest[],
+ ): Promise;
};
/**
diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md
index cbe31b02f3..aec6cb61b5 100644
--- a/plugins/scaffolder-backend-module-gitlab/report.api.md
+++ b/plugins/scaffolder-backend-module-gitlab/report.api.md
@@ -154,7 +154,7 @@ export const createGitlabRepoPushAction: (options: {
sourcePath?: string | undefined;
targetPath?: string | undefined;
token?: string | undefined;
- commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined;
+ commitAction?: 'auto' | 'update' | 'create' | 'delete' | undefined;
},
{
projectid: string;
@@ -271,7 +271,7 @@ export const createPublishGitlabMergeRequestAction: (options: {
sourcePath?: string | undefined;
targetPath?: string | undefined;
token?: string | undefined;
- commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined;
+ commitAction?: 'auto' | 'update' | 'create' | 'delete' | 'skip' | undefined;
projectid?: string | undefined;
removeSourceBranch?: boolean | undefined;
assignee?: string | undefined;
diff --git a/yarn.lock b/yarn.lock
index 63aff90ba1..80b34e1d42 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3628,10 +3628,12 @@ __metadata:
"@backstage/core-app-api": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/errors": "workspace:^"
+ "@backstage/filter-predicates": "workspace:^"
"@backstage/frontend-defaults": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
"@backstage/plugin-app": "workspace:^"
+ "@backstage/plugin-permission-common": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@backstage/version-bridge": "workspace:^"
@@ -3752,6 +3754,7 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
+ "@backstage/filter-predicates": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
"@backstage/test-utils": "workspace:^"
@@ -9941,6 +9944,7 @@ __metadata:
resolution: "@internal/frontend@workspace:packages/frontend-internal"
dependencies:
"@backstage/cli": "workspace:^"
+ "@backstage/filter-predicates": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"