feat(FR-3271): add detailed permissions tab with per-scope-type tables and grant-state tags#8191
feat(FR-3271): add detailed permissions tab with per-scope-type tables and grant-state tags#8191ironAiken2 wants to merge 3 commits into
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has required the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Coverage Report for react-coverage (./react)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
425e4ec to
b88365f
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces the new "Detailed Permissions" tab in RoleDetailDrawer, the base (view-only) PR of the FR-3264 stack. It merges the former Scopes view into a per-scope-type BAICard table layout where each row shows a concrete scope instance and color-coded, display-only grant-state tags per selectable entity type (green = fully / gold = partially / gray = not allowed). Each scope-type section self-fetches its rows, the role's grants, and (once at tab level) the rbacPermissionMatrix, so filtering/refreshing one card doesn't disturb others. This lands the at-a-glance read view; the scope-level edit modal follows in PRs 2–3.
Changes:
- New
RolePermissionAssignmentTab.tsx(per-scope-type sections, offset pagination in local React state, grant-state tags) plus the purerbacGrantState.tshelper and its unit tests. - Restructures
RoleDetailDrawerContent.tsx(default tab → Detailed Permissions, dropss*nuqs keys) andRoleDetailDrawer.tsx(removes scope query variables/fragment); deletes the absorbedRoleScopeTab.tsx. - Adds
rbac.DetailedPermissions,FullyAllowed,PartiallyAllowed,NotAllowed,NoScopesToDisplaytoen.json/ko.json.
Reviewed changes
Copilot reviewed 10 out of 17 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
react/src/components/RolePermissionAssignmentTab.tsx |
New merged tab: per-scope-type cards, self-owned queries, grant-state tag cells. |
react/src/helper/rbacGrantState.ts |
Pure computeRBACGrantState() (full/partial/none via matrix ∩ granted ops). |
react/src/helper/rbacGrantState.test.ts |
8 unit cases covering full/partial/none, empty matrix, dedup, stale grants. |
react/src/components/RoleDetailDrawerContent.tsx |
Adds default Detailed Permissions tab, removes Scopes tab + s* nuqs keys. |
react/src/components/RoleDetailDrawer.tsx |
Drops RoleScopeTabFragment spread and scope* query variables. |
react/src/components/RoleScopeTab.tsx |
Deleted (fully absorbed into the new tab). |
resources/i18n/en.json, resources/i18n/ko.json |
New rbac.* grant-state / empty-state labels. |
.specs/FR-3264-.../spec.md, dev-plan.md |
Design spec and phased PR-stack plan (docs only). |
react/src/__generated__/*.graphql.ts |
Relay compiler output consistent with the query/fragment changes. |
The change is clean and follows repository conventions, but it restructures GraphQL queries across multiple files in the security-adjacent RBAC surface, changes the default drawer tab, and relies on old-backend filter-wrapper branching and a documented 500-row tag-computation bound whose runtime behavior can't be fully verified statically. This warrants final human verification of the drawer end-to-end.
Files not reviewed (7)
- react/src/generated/RoleDetailDrawerContentFragment.graphql.ts: Generated file
- react/src/generated/RoleDetailDrawerQuery.graphql.ts: Generated file
- react/src/generated/RolePermissionAssignmentTabMatrixQuery.graphql.ts: Generated file
- react/src/generated/RolePermissionAssignmentTabSectionQuery.graphql.ts: Generated file
- react/src/generated/RolePermissionAssignmentTab_roleScopeFragment.graphql.ts: Generated file
- react/src/generated/RoleScopeTabFragment.graphql.ts: Generated file
- react/src/generated/RoleScopeTabRefetchQuery.graphql.ts: Generated file
b88365f to
f02006c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 36 changed files in this pull request and generated 1 comment.
Files not reviewed (7)
- react/src/generated/RoleDetailDrawerContentFragment.graphql.ts: Generated file
- react/src/generated/RoleDetailDrawerQuery.graphql.ts: Generated file
- react/src/generated/RolePermissionAssignmentTabMatrixQuery.graphql.ts: Generated file
- react/src/generated/RolePermissionAssignmentTabSectionQuery.graphql.ts: Generated file
- react/src/generated/RolePermissionAssignmentTab_roleScopeFragment.graphql.ts: Generated file
- react/src/generated/RoleScopeTabFragment.graphql.ts: Generated file
- react/src/generated/RoleScopeTabRefetchQuery.graphql.ts: Generated file
08e9413 to
2ea90cc
Compare
Coverage Report for backend-ai-ui-coverage (./packages/backend.ai-ui)
File CoverageNo changed files found. |
agatha197
left a comment
There was a problem hiding this comment.
Reviewed the Detailed Permissions tab. Solid refactor overall — fragment architecture, single $id query, pagination single-mode compliance, i18n key parity, and no dangling references to the deleted components all look good. No blockers. One primary request (fixed scope-type card ordering) plus a medium correctness caveat and a few cleanups, left inline.
2ea90cc to
ac195dd
Compare
…s and grant-state tags # Conflicts: # react/src/components/CreatePermissionModal.tsx # react/src/components/RolePermissionTab.tsx
…ush empty-state card body, correct stale permission-cap comment
- Remove always-undefined onDataChange/onAssignmentChange/onTabReset props
(RoleDetailDrawer, RoleDetailDrawerContent, RoleAssignmentTab)
- Add styles={{ body: { paddingTop: 0 } }} to the empty-state BAICard
(RolePermissionDetailTab)
- Correct PERMISSION_FETCH_LIMIT comment: PermissionFilter.scopeId { in }
exists on 26.8.0; exact paging deferred as follow-up (ScopedRolePermissionCard)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ScopedRolePermissionCard table: name column '적용 대상'→'이름' (rbac.Name,
new), raw id column '적용 대상 ID'→'ID' (general.ID), permissions column
'세부 권한'→'권한'
- Detailed Permissions tab label now reuses rbac.Permissions ('권한');
rbac.DetailedPermissions removed from all 21 locales
- rbac.ScopeRawId ko value '적용 대상 ID'→'적용 범위 ID' (role list column and
scope-id search label)
- 19 non-source locales retranslated for rbac.Name/Permissions/ScopeRawId
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
480b7e3 to
db65cf9
Compare
| import { useSuspendedBackendaiClient } from '../hooks'; | ||
| import RoleDetailDrawerContent from './RoleDetailDrawerContent'; | ||
| import RoleFormModal from './RoleFormModal'; | ||
| import { Alert, Drawer, Skeleton, Tooltip, Typography, theme } from 'antd'; |
There was a problem hiding this comment.
[Re: line +23]
roleId: string || null;
I think null would be better than undefined when the value is absent.
See this comment inline on Graphite.
| @@ -62,97 +61,43 @@ const RoleDetailDrawer: React.FC<RoleDetailDrawerProps> = ({ | |||
| interface RoleDetailDrawerInnerProps { | |||
There was a problem hiding this comment.
[Re: lines +52 to +56]
Instead of separating them, please use a deferred value and drawer.loading.
See this comment inline on Graphite.
| if (!adminRole) { | ||
| return ( | ||
| <Alert type="warning" showIcon message={t('general.ErrorOccurred')} /> | ||
| <Alert type="warning" showIcon description={t('general.ErrorOccurred')} /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Instead of this approach, the @required directive seems like a better fit.
There was a problem hiding this comment.
I don't think this function contains logic that warrants splitting it out.
| for (const operation of uniqueFullOperations) { | ||
| if (grantedOperations.has(operation)) { | ||
| grantedWithinMatrix += 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
Since it's a Set, the intersection method should be enough.

Resolves #8179 (FR-3271)
Summary
Introduces the new "Detailed Permissions" tab in
RoleDetailDrawer, merging the former Scopes and Permissions views into per-scope-type cards. This is PR 1 of the FR-3264 stack — the at-a-glance view (grant-state tags). The scope-level permission edit modal follows in PR 2/3.What changed
react/src/components/RolePermissionDetailTab.tsx— a thin orchestrator. Reads the role via a singleroleNodeFrgmt(id+totalScopes: scopes(first: 1) { count }for the tab-level empty state) and renders one card per scope type thatrbacPermissionMatrixreports — scope types are not hardcoded client-side, so a type added on the server needs no code change here. Each card gets its type's configurable entity × operation set derived from the matrix.react/src/components/ScopedRolePermissionCard.tsx— one scope-type card. Issues its own query from the first render (so per-card search/refresh never refetches the others, FR-3):adminRole.scopes(filter: { scopeType: { equals: <type> } }, limit, offset)— server-filtered by scope type and server-paginated via theEntityFilter.scopeTypefilter added in manager 26.8.0, withuseBAIPaginationOptionStatedrivinglimit/offset(offset mode only, per.claude/rules/graphql-pagination.md).adminRole.permissions(filter: { scopeType: ... }, limit: 100)— the role's grants for tag-state computation (the role is implicit via the connection; noroleIdfilter).BAIGraphQLPropertyFilter<EntityFilter>(scopeId, defaultiContains, 26.8.0) paired withBAIFetchKeyButtonin the card body (UserFolderPermissionPanelV2pattern). The resolved scope name is not searchable —EntityFilterexposes no name field (see Notes).count === 0and no active filter) — FR-2's "only assigned scope types" holds per card.BAINameActionCell(the FR-6 edit action attaches here in PR 2), raw scope id viaBAIId, and display-only entity-typeTags color-coded by grant state — green (all matrix operations granted), gold (some), gray (none — still shown, never hidden).react/src/helper/rbacGrantState.ts(+ unit test) — purecomputeRBACGrantState()comparing the role's granted operations against the full matrix operation set for a(scopeType, entityType)pair →full/partial/none.data/schema.graphqlsynced to manager 26.8.0 —EntityFiltergainsscopeType/scopeIdfilter fields ("Added in 26.8.0"), which is what makes server-side scope-type filtering possible (the previously escalated backend gap is resolved). The schema bump also refreshes unrelated generated artifacts.RoleScopeTab.tsx,RolePermissionTab.tsx, andCreatePermissionModal.tsxare deleted (superseded by the merged tab; permission editing returns as the edit modal in PR 2).RoleFormModalnow sourcesRBACElementTypefrom its own generated types.react/src/components/RoleAssignmentTab.tsx— fragment moved fromon Query(adminRoleAssignments(filter: { roleId })) toon Roleusing theRole.usersconnection; node-based refetch;roleId/projectScopeIdderived from its own fragment;a*nuqs keys → local React state (nothing inside the drawer writes to the URL — only the page-levelroleDetailparam remains); typedRoleAssignmentFilter.react/src/components/RoleDetailDrawer.tsx—RoleDetailDrawerQuerytakes only$id; all tab data flows throughRoleDetailDrawerContentFragmentonadminRole.roleNodeFrgmt(RoleDetailDrawerContent,RoleAssignmentTab,RolePermissionDetailTab,ScopedRolePermissionCard,RoleFormModal).baiClient.supports('rbac-filter-wrapper')guard is dropped in the new components; 26.4.x LTS always supports the wrapped operator form.rbac.DetailedPermissions,rbac.FullyAllowed,rbac.PartiallyAllowed,rbac.NotAllowed; rewordsrbac.NoScopesToDisplayto convey "no scopes are configured for this role"; removes the now-unusedrbac.SearchScopes— all 21 locales..specs/FR-3264-role-detail-drawer-detailed-permissions/(to be refreshed once the stack settles).Notes / caveats
EntityFilter.scopeType/scopeIdfilters. On older managers the scopes connection rejects/ignores these fields — the tab is not version-gated (the whole RBAC detail surface targets new managers).iContains); the resolved display name (project name / email / …) is not server-searchable. If name search is needed, it requires a backendEntityFiltername field (follow-up).adminRole.permissionshas no per-scopeId list filter to page exactly against the visible rows); a known bound for very large roles.Verification
bash scripts/verify.sh→ Relay, Lint, Format, TypeScript all pass. Unit testsreact/src/helper/rbacGrantState.test.tspass. Server-filter behavior (scopeType/scopeId iContains, server pagination) needs a live check against a 26.8.0 manager.Stacked on
This is the base PR of the FR-3264 stack (
main → FR-3271 → FR-3272 → FR-3273). PR 2 (FR-3272) adds the single-scope permission edit modal wired intoScopedRolePermissionCard; PR 3 (FR-3273) adds multi-scope bulk edit (row selection + 'Keep as is' semantics).Screenshots
Detailed Permissions tab — per-scope-type tables with grant-state tags
🤖 Generated with Claude Code