diff --git a/packages/app/src/examples/pagesPlugin.tsx b/packages/app/src/examples/pagesPlugin.tsx index d9d8bbb4c3..531d0084fb 100644 --- a/packages/app/src/examples/pagesPlugin.tsx +++ b/packages/app/src/examples/pagesPlugin.tsx @@ -426,6 +426,17 @@ const RestrictedCard = PermissionExampleCardBlueprint.make({ if: { permissions: { $contains: 'catalog.entity.create' } }, }); +// Feature flag-gated card — only instantiated when the user has +// the experimental-card FF enabled. +const FeatureFlagCard = PermissionExampleCardBlueprint.make({ + name: 'feature-flag', + params: { + title: 'Feature Flagged Card', + description: 'Visible only with the experimental-card FF active.', + }, + if: { featureFlags: { $contains: 'experimental-card' } }, +}); + // Example: Page enabled only when the user is allowed to create catalog entities. // // The `if` predicate is evaluated once at app startup (after sign-in), @@ -479,6 +490,7 @@ export const pagesPlugin = createFrontendPlugin({ { name: 'experimental-features' }, { name: 'advanced-features' }, { name: 'beta-access' }, + { name: 'experimental-card' }, ], extensions: [ IndexPage, @@ -491,5 +503,6 @@ export const pagesPlugin = createFrontendPlugin({ PublicCard, RestrictedCard, PermissionGatedPage, + FeatureFlagCard, ], }); diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index eb98150e64..e8fd21305a 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -45,6 +45,7 @@ "@backstage/config": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", + "dataloader": "^2.0.0", "swr": "^2.0.0" }, "devDependencies": { diff --git a/plugins/permission-react/src/apis/IdentityPermissionApi.ts b/plugins/permission-react/src/apis/IdentityPermissionApi.ts index 7a3ac6482e..6e848bd778 100644 --- a/plugins/permission-react/src/apis/IdentityPermissionApi.ts +++ b/plugins/permission-react/src/apis/IdentityPermissionApi.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import DataLoader from 'dataloader'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { PermissionApi } from './PermissionApi'; import { @@ -24,20 +25,27 @@ import { import { Config } from '@backstage/config'; /** - * The default implementation of the PermissionApi, which simply calls the authorize method of the given - * {@link @backstage/plugin-permission-common#PermissionClient}. + * The default implementation of the PermissionApi, which batches calls to + * {@link @backstage/plugin-permission-common#PermissionClient} that are made + * within the same microtask into a single HTTP request. * @public */ export class IdentityPermissionApi implements PermissionApi { - private readonly permissionClient: PermissionClient; - private readonly identityApi: IdentityApi; + private readonly loader: DataLoader< + AuthorizePermissionRequest, + AuthorizePermissionResponse + >; private constructor( permissionClient: PermissionClient, identityApi: IdentityApi, ) { - this.permissionClient = permissionClient; - this.identityApi = identityApi; + this.loader = new DataLoader( + async (requests: readonly AuthorizePermissionRequest[]) => { + const credentials = await identityApi.getCredentials(); + return permissionClient.authorize([...requests], credentials); + }, + ); } static create(options: { @@ -59,11 +67,9 @@ export class IdentityPermissionApi implements PermissionApi { 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 Array.isArray(request) ? responses : responses[0]; + if (Array.isArray(request)) { + return Promise.all(request.map(r => this.loader.load(r))); + } + return this.loader.load(request); } } diff --git a/yarn.lock b/yarn.lock index 80b34e1d42..191891148d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6454,6 +6454,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18.0.0" + dataloader: "npm:^2.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2"