/** * AccessRulesEditor * * Main component for editing RBAC access rules for a role. * Provides tabbed interface for DATA, UI, and RESOURCE rules. * * Features: * - Checkbox-based compact table for DATA rules * - Card view for UI/RESOURCE rules * - Object catalog dropdown for adding new rules */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { FaTable, FaDesktop, FaServer, FaCode, FaPlus, FaTrash, FaSave, FaUndo, FaSpinner, FaThList, FaTh, } from 'react-icons/fa'; import { useAccessRules, type AccessRule, type RuleContext, type AccessLevel, type AccessRuleCreate, } from '../../hooks/useAccessRules'; import { useCatalogObjects, type CatalogObject } from '../../hooks/useCatalogObjects'; import { AccessLevelSelect } from './AccessLevelSelect'; import { AccessRulesTable } from './AccessRulesTable'; import styles from './AccessRules.module.css'; // ============================================================================= // TYPES // ============================================================================= interface AccessRulesEditorProps { roleId: string; roleName?: string; isTemplate?: boolean; readOnly?: boolean; onSave?: () => void; apiBasePath?: string; mandateId?: string; featureCode?: string; // Filter catalog objects to this feature only } type TabType = 'DATA' | 'UI' | 'RESOURCE' | 'JSON'; // ============================================================================= // RULE CARD COMPONENT // ============================================================================= interface RuleCardProps { rule: AccessRule; readOnly?: boolean; onUpdate: (ruleId: string, updates: Partial) => void; onDelete: (ruleId: string) => void; } const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete }) => { const isDataRule = rule.context === 'DATA'; return (
{rule.context === 'DATA' ? : rule.context === 'UI' ? : } {rule.item || '(global)'}
{!readOnly && (
)}
{/* View Toggle */}
View
onUpdate(rule.id, { view: e.target.checked })} disabled={readOnly} className={styles.viewCheckbox} />
{/* CRUD Levels (only for DATA context) */} {isDataRule ? ( <>
Read onUpdate(rule.id, { read: value })} disabled={readOnly} compact />
Create onUpdate(rule.id, { create: value })} disabled={readOnly} compact />
Update onUpdate(rule.id, { update: value })} disabled={readOnly} compact />
Delete onUpdate(rule.id, { delete: value })} disabled={readOnly} compact />
) : ( // For UI and RESOURCE, show empty placeholders to maintain grid
)}
); }; // ============================================================================= // ADD RULE FORM // ============================================================================= interface AddRuleFormProps { context: RuleContext; availableObjects: CatalogObject[]; onAdd: (rule: AccessRuleCreate) => void; onCancel: () => void; } const AddRuleForm: React.FC = ({ context, availableObjects, onAdd, onCancel }) => { const [item, setItem] = useState(''); const [useCustom, setUseCustom] = useState(false); const [view, setView] = useState(true); const [read, setRead] = useState('n'); const [create, setCreate] = useState('n'); const [update, setUpdate] = useState('n'); const [del, setDel] = useState('n'); // Group objects by feature const groupedObjects = useMemo(() => { const grouped: Record = {}; availableObjects.forEach(obj => { if (!grouped[obj.featureCode]) { grouped[obj.featureCode] = []; } grouped[obj.featureCode].push(obj); }); return grouped; }, [availableObjects]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const newRule: AccessRuleCreate = { context, item: item.trim() || null, view, ...(context === 'DATA' ? { read, create, update, delete: del } : {}), }; onAdd(newRule); }; const getPlaceholder = () => { switch (context) { case 'DATA': return 'z.B. data.feature.trustee.TrusteePosition'; case 'UI': return 'z.B. ui.feature.trustee.dashboard'; case 'RESOURCE': return 'z.B. resource.feature.trustee.documents.create'; } }; const getLabel = (obj: CatalogObject): string => { return obj.label.de || obj.label.en || obj.objectKey; }; return (
{useCustom ? ( setItem(e.target.value)} placeholder={getPlaceholder()} className={styles.formInput} autoFocus /> ) : ( )} Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).
{context === 'DATA' && (
{/* Header Row */}
Eigene (m)
Gruppe (g)
Alle (a)
{/* CRUD Rows */} {(['create', 'read', 'update', 'delete'] as const).map(op => { const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read; const setValue = op === 'delete' ? setDel : op === 'create' ? setCreate : op === 'update' ? setUpdate : setRead; const labels = { create: 'Create', read: 'Read', update: 'Update', delete: 'Delete' }; return (
{labels[op]}
{(['m', 'g', 'a'] as const).map(level => (
{ if (e.target.checked) { setValue(level); } else { // Deactivate: set to level below const hierarchy: AccessLevel[] = ['n', 'm', 'g', 'a']; const idx = hierarchy.indexOf(level); setValue(hierarchy[idx - 1] || 'n'); } }} title={`${labels[op]} - ${level === 'm' ? 'Eigene' : level === 'g' ? 'Gruppe' : 'Alle'}`} />
))}
); })}
)}
); }; // ============================================================================= // RULES SECTION // ============================================================================= interface RulesSectionProps { context: RuleContext; rules: AccessRule[]; availableObjects: CatalogObject[]; readOnly?: boolean; onUpdate: (ruleId: string, updates: Partial) => void; onDelete: (ruleId: string) => void; onAdd: (rule: AccessRuleCreate) => void; } const RulesSection: React.FC = ({ context, rules, availableObjects, readOnly, onUpdate, onDelete, onAdd, }) => { const [showAddForm, setShowAddForm] = useState(false); const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA const handleAdd = (rule: AccessRuleCreate) => { onAdd(rule); setShowAddForm(false); }; const getEmptyIcon = () => { switch (context) { case 'DATA': return ; case 'UI': return ; case 'RESOURCE': return ; } }; const getEmptyText = () => { switch (context) { case 'DATA': return 'Keine Daten-Regeln definiert'; case 'UI': return 'Keine UI-Regeln definiert'; case 'RESOURCE': return 'Keine Ressourcen-Regeln definiert'; } }; return (
{!readOnly && !showAddForm && (
{rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'}
{/* View Toggle */} {context === 'DATA' && rules.length > 0 && ( <> )}
)} {showAddForm && ( setShowAddForm(false)} /> )} {rules.length === 0 && !showAddForm ? (
{getEmptyIcon()}

{getEmptyText()}

{!readOnly && (

Klicken Sie auf "Neue Regel" um eine Berechtigung hinzuzufügen.

)}
) : useTableView && context === 'DATA' ? ( ) : ( rules.map(rule => ( )) )}
); }; // ============================================================================= // JSON EDITOR // ============================================================================= interface JsonEditorProps { rules: AccessRule[]; readOnly?: boolean; onApply: (rules: AccessRule[]) => void; } const JsonEditor: React.FC = ({ rules, readOnly, onApply }) => { const [jsonText, setJsonText] = useState(''); const [error, setError] = useState(null); useEffect(() => { setJsonText(JSON.stringify(rules, null, 2)); setError(null); }, [rules]); const handleApply = () => { try { const parsed = JSON.parse(jsonText); if (!Array.isArray(parsed)) { throw new Error('JSON muss ein Array sein'); } setError(null); onApply(parsed); } catch (err: any) { setError(err.message); } }; return (