Skip to content

FRD-035: Role Assignment Panel

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
RelatedADR-027 (Default Tech Stack)
Target Releasev2.0.0
TypeWidget
SizeM
PriorityP1 — SaaS Widget

Document Summary

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

ItemDescription
Member listDataGrid with avatar, name, email, current role, and role selector columns.
Role selectorPer-row Select dropdown populated from consumer-provided roles.
Diff viewSummary panel showing “N changes pending” with a list of old-role → new-role changes per affected member.
Cancel/Save flowCancel reverts all pending changes; Save calls onSave with the change set.
SearchFilter members by name or email.
State handlingLoading skeleton, empty state, error state with retry.
Stories and testsStorybook stories for all states; unit tests for role changes, diff, cancel, save.

Out of Scope

ItemReason
Role definition/creationSeparate admin concern; widget consumes a fixed role list.
Permission matrixDifferent widget pattern; this widget is about assignment, not definition.
Member removalDestructive action handled by InviteFlow or a separate “Remove member” action.

Users and Pain Points

UserPain Point
SaaS adminsNo standard widget for managing team member roles; each product builds its own.
Security-conscious teamsRole changes happen without a review step; accidental promotions to admin are risky.
DevelopersBuilding role management with diff-before-save is complex to implement correctly.
Teams with many membersNo search/filter on role management screens; finding a specific member requires scrolling.

Definitions

TermDefinition
Role assignmentThe act of assigning a permission role to a team member (e.g., making a user an “Admin”).
Diff viewA summary of pending changes showing the before and after state for each modified role.
Change setThe 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).
  • DataGrid: Full-featured table primitive.
  • Select: Dropdown selector for role choice.
  • Avatar: Member avatar display.
  • No generic role assignment panel exists.

Proposed Solution

Build src/components/widgets/role-assignment-panel.tsx:

Member List

DataGrid with columns:

  1. Avatar + Name + Email — Combined cell (avatar, name, email like PeopleTable).
  2. Current Role — Badge showing the persisted role (read-only reference).
  3. New Role — Select dropdown pre-selected to current role; changes highlight the row.
  4. 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

IDRequirementPriority
FR-01Widget displays a DataGrid with avatar, name, email, and role selector per member.Must
FR-02Role selector is populated from consumer-provided roles array.Must
FR-03Changing a role highlights the row to indicate a pending change.Should
FR-04A 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-08Save button is disabled when no changes are pending.Must
FR-09Reverting a role to its original value removes it from the change set.Must
FR-10Search input filters members by name or email.Should
FR-11Loading state shows skeleton rows.Must
FR-12Empty state shows “No team members” message.Must

Non-Functional Requirements

IDRequirementTarget
NFR-01Renders 100 members with role selectors in < 100ms.Measured via React Profiler.
NFR-02Bundle size< 6 KB gzipped (excluding shared deps).
NFR-03Change tracking overhead< 1ms per role change (map lookup).
NFR-04Dark modeFull token-based dark mode support.
NFR-05ResponsiveRole 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

IDRequirement
A11Y-01Role Select dropdowns have aria-label identifying the member whose role is being changed.
A11Y-02Rows with pending changes are announced to screen readers via aria-label change indicator.
A11Y-03Diff view summary is focusable and readable by screen readers.
A11Y-04Footer bar save/cancel buttons are keyboard accessible.
A11Y-05Search input has aria-label="Search team members".
A11Y-06All components pass axe-core automated checks with zero violations.

Content and Documentation Requirements

IDRequirement
DOC-01Storybook docs page with usage guidelines, prop table, and interactive examples.
DOC-02Recipe showing RoleAssignmentPanel + InviteFlow (FRD-034) composed in a team settings page.
DOC-03Guide on defining roles and mapping them to backend permission models.

Dependencies

DependencyTypeRisk
src/components/ui/data-grid.tsxInternalLow — table primitive.
src/components/ui/select.tsxInternalLow — role selector.
src/components/ui/avatar.tsxInternalLow — member avatars.
src/components/ui/badge.tsxInternalLow — role badges.
src/components/ui/button.tsxInternalLow — save/cancel actions.
src/components/ui/status-card.tsxInternalLow — empty/error states.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Select dropdown inside DataGrid row causes z-index issuesMediumMediumEnsure dropdown popover has proper z-index above the table; test with scrolling.
Large teams (500+ members) make the role selector per row expensiveLowMediumVirtualize rows via DataGrid; lazy-render Select dropdowns.
Diff view is confusing for users making many changesLowLowGroup changes by role transition (e.g., “3 members: Member → Admin”).

Open Questions

#QuestionOwnerStatus
OQ-01Should the widget support bulk role assignment (select multiple → assign role)?David HolmesOpen
OQ-02Should the diff view be a popover, a modal, or an inline expandable section?David HolmesOpen
OQ-03Should the widget support a “Remove member” action alongside role changes?David HolmesOpen

Acceptance Criteria

#Criterion
AC-01Widget renders a table with avatar, name, email, and role selector per member.
AC-02Changing a role highlights the row and increments the pending change count.
AC-03Reverting a role to its original value removes it from the change set.
AC-04Diff view shows old role → new role for each changed member.
AC-05Cancel reverts all changes; Save calls onSave with the change set.
AC-06Loading, empty, and error states render appropriate visual treatments.
AC-07All components pass axe-core checks with zero violations.
AC-08Storybook stories exist for all states and interaction flows.
AC-09pnpm typecheck and pnpm vitest run --project unit pass with zero errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Create src/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.
  2. DataGrid columns: Member (Avatar + name + email), Current Role (Badge, read-only), New Role (Select dropdown), Change indicator (dot or highlight).
  3. Change tracking: Use a Map&lt;string, RoleChange&gt; in state. On Select change, add/update the map entry. If new role equals original role, delete the entry.
  4. Footer bar: Render a sticky bottom bar when changeMap.size > 0. Show count, Review button, Cancel button, Save button.
  5. Diff view: On “Review Changes”, render a list mapping over changeMap.values() showing member name and old → new role with arrow icon.
  6. Cancel: Clear the change map and reset all Select values to original roles.
  7. Save: Convert map to array, call onSave(changes), show loading state on button.
  8. Stories in src/components/widgets/role-assignment-panel.stories.tsx. Include: Default, WithPendingChanges, DiffView, Loading, Empty, Error.
  9. 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.
  • src/components/ui/select.tsx — role dropdown.
  • src/components/ui/data-grid.tsx — table primitive.

Decision Log

DateDecisionRationale
2026-05-26Generic role assignment rather than extending rbac-role-table.rbac-role-table is K8s-specific; SaaS teams need a domain-agnostic widget.
2026-05-26Diff-before-save rather than auto-save per row.Prevents accidental role changes; admin reviews all changes in batch.
2026-05-26Internal change tracking with Map rather than requiring consumer to manage dirty state.Reduces consumer complexity; widget is self-contained.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.