Build a Role Assignment Panel widget providing a DataGrid with avatar, name, and role columns where each row has a role selector. The widget supports a diff-view showing pending changes before save, a cancel/save flow, and ships with Storybook stories. It generalizes the pattern from the K8s-specific rbac-role-table.tsx into a generic team/organization role management widget.
Introduction
Overview
Managing team member roles is a core SaaS pattern. The design system has rbac-role-table.tsx for Kubernetes RBAC bindings, but it is domain-specific and tightly coupled to K8s concepts (namespaces, environments, subjects). A generic Role Assignment Panel is needed for any multi-user application where admins assign roles to team members.
Goals
Provide a RoleAssignmentPanel widget showing team members with editable role assignments.
Display avatar, name, email, and current role per member.
Allow role changes via a per-row Select dropdown.
Show a diff-view summarizing all pending changes before save.
Provide cancel (revert all changes) and save (commit changes via callback) actions.
Handle loading, empty, and error states.
Non-Goals
Permission/capability management (defining what each role can do).
RBAC policy editing (Kubernetes-specific RBAC stays in rbac-role-table.tsx).
User invitation (covered by InviteFlow FRD-034).
Scope
In Scope
Item
Description
Member list
DataGrid with avatar, name, email, current role, and role selector columns.
Role selector
Per-row Select dropdown populated from consumer-provided roles.
Diff view
Summary panel showing “N changes pending” with a list of old-role → new-role changes per affected member.
Cancel/Save flow
Cancel reverts all pending changes; Save calls onSave with the change set.
Search
Filter members by name or email.
State handling
Loading skeleton, empty state, error state with retry.
Stories and tests
Storybook stories for all states; unit tests for role changes, diff, cancel, save.
Out of Scope
Item
Reason
Role definition/creation
Separate admin concern; widget consumes a fixed role list.
Permission matrix
Different widget pattern; this widget is about assignment, not definition.
Member removal
Destructive action handled by InviteFlow or a separate “Remove member” action.
Users and Pain Points
User
Pain Point
SaaS admins
No standard widget for managing team member roles; each product builds its own.
Security-conscious teams
Role changes happen without a review step; accidental promotions to admin are risky.
Developers
Building role management with diff-before-save is complex to implement correctly.
Teams with many members
No search/filter on role management screens; finding a specific member requires scrolling.
Definitions
Term
Definition
Role assignment
The act of assigning a permission role to a team member (e.g., making a user an “Admin”).
Diff view
A summary of pending changes showing the before and after state for each modified role.
Change set
The collection of role changes to be saved: { memberId: string; oldRole: string; newRole: string }[].
Current State
RbacRoleTable (src/components/widgets/sre-devops/rbac-role-table.tsx): K8s-specific RBAC table with subject, role, environment, namespace columns. Has a revoke action but no role editing or diff view.
PeopleTable (src/components/widgets/people-table.tsx): Team member table with avatar, name, email, role, status. Role is display-only (not editable).
Avatar + Name + Email — Combined cell (avatar, name, email like PeopleTable).
Current Role — Badge showing the persisted role (read-only reference).
New Role — Select dropdown pre-selected to current role; changes highlight the row.
Status indicator — A subtle dot or highlight on rows with pending changes.
Diff View
When any role has been changed, a sticky footer bar appears:
“N changes pending” text.
“Review Changes” button opens a summary showing each changed member’s name, old role → new role.
“Cancel” button reverts all changes.
“Save Changes” button calls onSave(changeSet).
The diff view can be a popover from the footer bar or an inline expandable section.
Change Tracking
The widget maintains internal state for pending changes. When a role selector changes, the widget records { memberId, oldRole, newRole }. If the user reverts a role back to its original value, the change is removed from the set. The save button is disabled when the change set is empty.
Requirements
Requirement Priorities
Must Have: Member list with role selectors, save callback with change set.
Should Have: Diff view before save, cancel to revert, row highlighting for changes.
Could Have: Search/filter, bulk role assignment, undo after save.
Functional Requirements
ID
Requirement
Priority
FR-01
Widget displays a DataGrid with avatar, name, email, and role selector per member.
Must
FR-02
Role selector is populated from consumer-provided roles array.
Must
FR-03
Changing a role highlights the row to indicate a pending change.
Should
FR-04
A footer bar shows “N changes pending” when changes exist.
Should
FR-05
”Review Changes” shows a diff summary: member name, old role → new role per change.
Should
FR-06
”Cancel” reverts all pending changes to the original role assignments.
Must
FR-07
”Save Changes” calls onSave with the change set array.
Must
FR-08
Save button is disabled when no changes are pending.
Must
FR-09
Reverting a role to its original value removes it from the change set.
Must
FR-10
Search input filters members by name or email.
Should
FR-11
Loading state shows skeleton rows.
Must
FR-12
Empty state shows “No team members” message.
Must
Non-Functional Requirements
ID
Requirement
Target
NFR-01
Renders 100 members with role selectors in < 100ms.
Measured via React Profiler.
NFR-02
Bundle size
< 6 KB gzipped (excluding shared deps).
NFR-03
Change tracking overhead
< 1ms per role change (map lookup).
NFR-04
Dark mode
Full token-based dark mode support.
NFR-05
Responsive
Role selector stacks below name on narrow screens.
API/Interface Requirements
interface TeamMember {
id:string;
name:string;
email?:string;
avatarSrc?:string;
roleId:string;
}
interface Role {
id:string;
label:string;
description?:string;
}
interface RoleChange {
memberId:string;
memberName:string;
oldRoleId:string;
oldRoleLabel:string;
newRoleId:string;
newRoleLabel:string;
}
interface RoleAssignmentPanelProps {
members:TeamMember[];
roles:Role[];
loading?:boolean;
error?:string;
onRetry?:()=>void;
onSave:(changes:RoleChange[])=>Promise<void>;
searchPlaceholder?:string;
emptyTitle?:string;
emptyDescription?:string;
className?:string;
}
Accessibility Requirements
ID
Requirement
A11Y-01
Role Select dropdowns have aria-label identifying the member whose role is being changed.
A11Y-02
Rows with pending changes are announced to screen readers via aria-label change indicator.
A11Y-03
Diff view summary is focusable and readable by screen readers.
A11Y-04
Footer bar save/cancel buttons are keyboard accessible.
A11Y-05
Search input has aria-label="Search team members".
A11Y-06
All components pass axe-core automated checks with zero violations.
Content and Documentation Requirements
ID
Requirement
DOC-01
Storybook docs page with usage guidelines, prop table, and interactive examples.
DOC-02
Recipe showing RoleAssignmentPanel + InviteFlow (FRD-034) composed in a team settings page.
DOC-03
Guide on defining roles and mapping them to backend permission models.
Ensure dropdown popover has proper z-index above the table; test with scrolling.
Large teams (500+ members) make the role selector per row expensive
Low
Medium
Virtualize rows via DataGrid; lazy-render Select dropdowns.
Diff view is confusing for users making many changes
Low
Low
Group changes by role transition (e.g., “3 members: Member → Admin”).
Open Questions
#
Question
Owner
Status
OQ-01
Should the widget support bulk role assignment (select multiple → assign role)?
David Holmes
Open
OQ-02
Should the diff view be a popover, a modal, or an inline expandable section?
David Holmes
Open
OQ-03
Should the widget support a “Remove member” action alongside role changes?
David Holmes
Open
Acceptance Criteria
#
Criterion
AC-01
Widget renders a table with avatar, name, email, and role selector per member.
AC-02
Changing a role highlights the row and increments the pending change count.
AC-03
Reverting a role to its original value removes it from the change set.
AC-04
Diff view shows old role → new role for each changed member.
AC-05
Cancel reverts all changes; Save calls onSave with the change set.
AC-06
Loading, empty, and error states render appropriate visual treatments.
AC-07
All components pass axe-core checks with zero violations.
AC-08
Storybook stories exist for all states and interaction flows.
AC-09
pnpm typecheck and pnpm vitest run --project unit pass with zero errors.
LLM Handoff Instructions
When implementing this FRD:
Createsrc/components/widgets/role-assignment-panel.tsx. Reference people-table.tsx for the avatar + name + email cell pattern and rbac-role-table.tsx for the role column pattern.
DataGrid columns: Member (Avatar + name + email), Current Role (Badge, read-only), New Role (Select dropdown), Change indicator (dot or highlight).
Change tracking: Use a Map<string, RoleChange> in state. On Select change, add/update the map entry. If new role equals original role, delete the entry.
Footer bar: Render a sticky bottom bar when changeMap.size > 0. Show count, Review button, Cancel button, Save button.
Diff view: On “Review Changes”, render a list mapping over changeMap.values() showing member name and old → new role with arrow icon.
Cancel: Clear the change map and reset all Select values to original roles.
Save: Convert map to array, call onSave(changes), show loading state on button.
Stories in src/components/widgets/role-assignment-panel.stories.tsx. Include: Default, WithPendingChanges, DiffView, Loading, Empty, Error.
Tests in src/components/widgets/role-assignment-panel.test.tsx. Test change tracking, revert-to-original cleanup, cancel, save callback.
Key files:
src/components/widgets/people-table.tsx — avatar + name + email cell pattern.
src/components/widgets/sre-devops/rbac-role-table.tsx — role column pattern.