feat: add role create and delete with system role protection
- Add create role form with name, description, and scope fields - Add delete button on role detail view for non-system roles - Use ConfirmDeleteDialog for safe deletion confirmation - System roles protected from deletion (button hidden) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useRoles, useRole } from '../../../api/queries/admin/rbac';
|
||||
import { useRoles, useRole, useCreateRole, useDeleteRole } from '../../../api/queries/admin/rbac';
|
||||
import type { RoleDetail } from '../../../api/queries/admin/rbac';
|
||||
import { ConfirmDeleteDialog } from '../../../components/admin/ConfirmDeleteDialog';
|
||||
import styles from './RbacPage.module.css';
|
||||
|
||||
function getInitials(name: string): string {
|
||||
@@ -19,6 +20,12 @@ export function RolesTab() {
|
||||
const roles = useRoles();
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newDesc, setNewDesc] = useState('');
|
||||
const [newScope, setNewScope] = useState('custom');
|
||||
const [createError, setCreateError] = useState('');
|
||||
const createRole = useCreateRole();
|
||||
|
||||
const roleDetail = useRole(selectedId);
|
||||
|
||||
@@ -48,6 +55,7 @@ export function RolesTab() {
|
||||
Define permission scopes; assign to users or groups
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className={styles.btnAdd} onClick={() => setShowCreateForm(true)}>+ Add role</button>
|
||||
</div>
|
||||
<div className={styles.split}>
|
||||
<div className={styles.listPane}>
|
||||
@@ -59,6 +67,39 @@ export function RolesTab() {
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{showCreateForm && (
|
||||
<div className={styles.createForm}>
|
||||
<div className={styles.createFormRow}>
|
||||
<label className={styles.createFormLabel}>Name</label>
|
||||
<input className={styles.createFormInput} value={newName}
|
||||
onChange={e => { setNewName(e.target.value); setCreateError(''); }}
|
||||
placeholder="Role name" autoFocus />
|
||||
</div>
|
||||
<div className={styles.createFormRow}>
|
||||
<label className={styles.createFormLabel}>Desc</label>
|
||||
<input className={styles.createFormInput} value={newDesc}
|
||||
onChange={e => setNewDesc(e.target.value)} placeholder="Optional description" />
|
||||
</div>
|
||||
<div className={styles.createFormRow}>
|
||||
<label className={styles.createFormLabel}>Scope</label>
|
||||
<input className={styles.createFormInput} value={newScope}
|
||||
onChange={e => setNewScope(e.target.value)} placeholder="custom" />
|
||||
</div>
|
||||
{createError && <div className={styles.createFormError}>{createError}</div>}
|
||||
<div className={styles.createFormActions}>
|
||||
<button type="button" className={styles.createFormBtn}
|
||||
onClick={() => { setShowCreateForm(false); setNewName(''); setNewDesc(''); setNewScope('custom'); setCreateError(''); }}>Cancel</button>
|
||||
<button type="button" className={styles.createFormBtnPrimary}
|
||||
disabled={!newName.trim() || createRole.isPending}
|
||||
onClick={() => {
|
||||
createRole.mutate({ name: newName.trim(), description: newDesc || undefined, scope: newScope || undefined }, {
|
||||
onSuccess: () => { setShowCreateForm(false); setNewName(''); setNewDesc(''); setNewScope('custom'); setCreateError(''); },
|
||||
onError: (err) => setCreateError(err instanceof Error ? err.message : 'Failed to create role'),
|
||||
});
|
||||
}}>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.entityList}>
|
||||
{filtered.map((role) => {
|
||||
const isSelected = role.id === selectedId;
|
||||
@@ -101,7 +142,7 @@ export function RolesTab() {
|
||||
<span>Select a role to view details</span>
|
||||
</div>
|
||||
) : (
|
||||
<RoleDetailView role={detail} />
|
||||
<RoleDetailView role={detail} onDeselect={() => setSelectedId(null)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,15 +150,26 @@ export function RolesTab() {
|
||||
);
|
||||
}
|
||||
|
||||
function RoleDetailView({ role }: { role: RoleDetail }) {
|
||||
function RoleDetailView({ role, onDeselect }: { role: RoleDetail; onDeselect: () => void }) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const deleteRole = useDeleteRole();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${styles.detailAvatar} ${styles.avatarRole}`} style={{ borderRadius: 8 }}>
|
||||
{getInitials(role.name)}
|
||||
</div>
|
||||
<div className={styles.detailName}>
|
||||
{role.name}
|
||||
{role.system && <span className={styles.lockIcon}>🔒</span>}
|
||||
<div className={styles.detailHeader}>
|
||||
<div className={styles.detailHeaderInfo}>
|
||||
<div className={`${styles.detailAvatar} ${styles.avatarRole}`} style={{ borderRadius: 8 }}>
|
||||
{getInitials(role.name)}
|
||||
</div>
|
||||
<div className={styles.detailName}>
|
||||
{role.name}
|
||||
{role.system && <span className={styles.lockIcon}>🔒</span>}
|
||||
</div>
|
||||
</div>
|
||||
{!role.system && (
|
||||
<button type="button" className={styles.btnDelete}
|
||||
onClick={() => setShowDeleteDialog(true)} disabled={deleteRole.isPending}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.detailEmail}>{role.description || 'No description'}</div>
|
||||
|
||||
@@ -196,6 +248,12 @@ function RoleDetailView({ role }: { role: RoleDetail }) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!role.system && (
|
||||
<ConfirmDeleteDialog isOpen={showDeleteDialog} onClose={() => setShowDeleteDialog(false)}
|
||||
onConfirm={() => { deleteRole.mutate(role.id, { onSuccess: () => { setShowDeleteDialog(false); onDeselect(); } }); }}
|
||||
resourceName={role.name} resourceType="role" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user