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:
hsiegeln
2026-03-17 18:34:46 +01:00
parent 4821ddebba
commit db6143f9da

View File

@@ -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}>&#128274;</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}>&#128274;</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" />
)}
</>
);
}