Org Roles Form
Install this component from the Scintillar registry.
Roles & Permissions
Define roles and assign permissions to control access across your organization.
OwnerBuilt-in5 permissions
Full access to everything
Admin4 permissions
Manage resources and members
Member2 permissions
Basic access
Source
"use client"
import { useState } from "react"
import { Plus, Trash2, Shield, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { FormSection } from "@/registry/new-york/blocks/form-section/form-section"
/** A permission that can be granted to a role. */
export interface Permission {
/** Unique permission ID. */
id: string
/** Display label. */
label: string
/** Description of what this permission allows. */
description?: string
/** Permission category/group. */
category: string
}
/** A role with its assigned permissions. */
export interface Role {
/** Unique role ID. */
id: string
/** Display name. */
name: string
/** Role description. */
description?: string
/** Whether this is a built-in role that cannot be deleted. */
builtIn?: boolean
/** IDs of permissions granted to this role. */
permissions: string[]
}
export interface OrgRolesFormProps {
/** Available roles. */
roles: Role[]
/** Available permissions. */
permissions: Permission[]
/** Called when roles are updated. */
onRolesChange?: (roles: Role[]) => void
/** Called when a new role is created. */
onCreateRole?: () => void
/** Called when a role is deleted. */
onDeleteRole?: (roleId: string) => void
/** Additional CSS classes. */
className?: string
}
/** Organization role and permission management form with an RBAC permission matrix. */
export function OrgRolesForm({
roles,
permissions,
onRolesChange,
onCreateRole,
onDeleteRole,
className,
}: OrgRolesFormProps) {
const [expandedRole, setExpandedRole] = useState<string | null>(null)
// Group permissions by category
const categories = Array.from(new Set(permissions.map((p) => p.category)))
const permsByCategory = categories.map((cat) => ({
category: cat,
permissions: permissions.filter((p) => p.category === cat),
}))
function togglePermission(roleId: string, permId: string) {
const updated = roles.map((r) => {
if (r.id !== roleId) return r
const has = r.permissions.includes(permId)
return {
...r,
permissions: has
? r.permissions.filter((p) => p !== permId)
: [...r.permissions, permId],
}
})
onRolesChange?.(updated)
}
return (
<FormSection
title="Roles & Permissions"
description="Define roles and assign permissions to control access across your organization."
className={className}
footer={
<Button onClick={onCreateRole}>
<Plus className="size-4 mr-1.5" />
Create role
</Button>
}
>
<div className="space-y-4">
{/* Roles list */}
{roles.map((role) => (
<div key={role.id} className="rounded-lg border">
{/* Role header */}
<div
className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/30 transition-colors"
onClick={() => setExpandedRole(expandedRole === role.id ? null : role.id)}
>
<Shield className="size-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{role.name}</span>
{role.builtIn && <Badge variant="secondary" className="text-[10px]">Built-in</Badge>}
<Badge variant="outline" className="text-[10px]">
{role.permissions.length} permission{role.permissions.length !== 1 ? "s" : ""}
</Badge>
</div>
{role.description && (
<p className="text-xs text-muted-foreground mt-0.5">{role.description}</p>
)}
</div>
<div className="flex items-center gap-1">
{!role.builtIn && onDeleteRole && (
<Button
variant="ghost"
size="icon-xs"
onClick={(e) => { e.stopPropagation(); onDeleteRole(role.id) }}
aria-label={`Delete ${role.name}`}
>
<Trash2 className="size-3.5 text-muted-foreground" />
</Button>
)}
<ChevronDown className={cn(
"size-4 text-muted-foreground transition-transform",
expandedRole === role.id && "rotate-180"
)} />
</div>
</div>
{/* Permission matrix */}
{expandedRole === role.id && (
<div className="border-t">
{permsByCategory.map((group) => (
<div key={group.category}>
<div className="px-3 py-2 bg-muted/20">
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{group.category}
</span>
</div>
{group.permissions.map((perm) => (
<div
key={perm.id}
className="flex items-center gap-3 px-3 py-2 border-t border-border/50 hover:bg-muted/10"
>
<Checkbox
id={`${role.id}-${perm.id}`}
checked={role.permissions.includes(perm.id)}
onCheckedChange={() => togglePermission(role.id, perm.id)}
disabled={role.builtIn}
/>
<div className="flex-1 min-w-0">
<Label
htmlFor={`${role.id}-${perm.id}`}
className="text-sm cursor-pointer"
>
{perm.label}
</Label>
{perm.description && (
<p className="text-xs text-muted-foreground">{perm.description}</p>
)}
</div>
</div>
))}
</div>
))}
</div>
)}
</div>
))}
{roles.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-6">
No roles defined yet. Create one to get started.
</p>
)}
</div>
</FormSection>
)
}