762 lines
23 KiB
TypeScript
762 lines
23 KiB
TypeScript
/**
|
|
* 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<AccessRule>) => void;
|
|
onDelete: (ruleId: string) => void;
|
|
}
|
|
|
|
const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete }) => {
|
|
const isDataRule = rule.context === 'DATA';
|
|
|
|
return (
|
|
<div className={styles.ruleCard}>
|
|
<div className={styles.ruleHeader}>
|
|
<div className={styles.ruleItem}>
|
|
<span className={styles.ruleItemIcon}>
|
|
{rule.context === 'DATA' ? <FaTable /> :
|
|
rule.context === 'UI' ? <FaDesktop /> : <FaServer />}
|
|
</span>
|
|
<span className={styles.ruleItemName}>{rule.item || '(global)'}</span>
|
|
</div>
|
|
{!readOnly && (
|
|
<div className={styles.ruleActions}>
|
|
<button
|
|
className={`${styles.iconButton} ${styles.danger}`}
|
|
onClick={() => onDelete(rule.id)}
|
|
title="Regel löschen"
|
|
>
|
|
<FaTrash />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.permissionsGrid}>
|
|
{/* View Toggle */}
|
|
<div className={styles.permissionItem}>
|
|
<span className={styles.permissionLabel}>View</span>
|
|
<div className={styles.viewToggle}>
|
|
<input
|
|
type="checkbox"
|
|
checked={rule.view}
|
|
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
|
|
disabled={readOnly}
|
|
className={styles.viewCheckbox}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CRUD Levels (only for DATA context) */}
|
|
{isDataRule ? (
|
|
<>
|
|
<div className={styles.permissionItem}>
|
|
<span className={styles.permissionLabel}>Read</span>
|
|
<AccessLevelSelect
|
|
value={rule.read}
|
|
onChange={(value) => onUpdate(rule.id, { read: value })}
|
|
disabled={readOnly}
|
|
compact
|
|
/>
|
|
</div>
|
|
<div className={styles.permissionItem}>
|
|
<span className={styles.permissionLabel}>Create</span>
|
|
<AccessLevelSelect
|
|
value={rule.create}
|
|
onChange={(value) => onUpdate(rule.id, { create: value })}
|
|
disabled={readOnly}
|
|
compact
|
|
/>
|
|
</div>
|
|
<div className={styles.permissionItem}>
|
|
<span className={styles.permissionLabel}>Update</span>
|
|
<AccessLevelSelect
|
|
value={rule.update}
|
|
onChange={(value) => onUpdate(rule.id, { update: value })}
|
|
disabled={readOnly}
|
|
compact
|
|
/>
|
|
</div>
|
|
<div className={styles.permissionItem}>
|
|
<span className={styles.permissionLabel}>Delete</span>
|
|
<AccessLevelSelect
|
|
value={rule.delete}
|
|
onChange={(value) => onUpdate(rule.id, { delete: value })}
|
|
disabled={readOnly}
|
|
compact
|
|
/>
|
|
</div>
|
|
</>
|
|
) : (
|
|
// For UI and RESOURCE, show empty placeholders to maintain grid
|
|
<div style={{ gridColumn: 'span 4' }} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// ADD RULE FORM
|
|
// =============================================================================
|
|
|
|
interface AddRuleFormProps {
|
|
context: RuleContext;
|
|
availableObjects: CatalogObject[];
|
|
onAdd: (rule: AccessRuleCreate) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, onAdd, onCancel }) => {
|
|
const [item, setItem] = useState('');
|
|
const [useCustom, setUseCustom] = useState(false);
|
|
const [view, setView] = useState(true);
|
|
const [read, setRead] = useState<AccessLevel>('n');
|
|
const [create, setCreate] = useState<AccessLevel>('n');
|
|
const [update, setUpdate] = useState<AccessLevel>('n');
|
|
const [del, setDel] = useState<AccessLevel>('n');
|
|
|
|
// Group objects by feature
|
|
const groupedObjects = useMemo(() => {
|
|
const grouped: Record<string, CatalogObject[]> = {};
|
|
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 (
|
|
<form className={styles.addRuleForm} onSubmit={handleSubmit}>
|
|
<div className={styles.formGroup}>
|
|
<div className={styles.objectSelectorLabel}>
|
|
<label className={styles.formLabel}>Objekt auswählen</label>
|
|
<button
|
|
type="button"
|
|
className={styles.toggleCustomButton}
|
|
onClick={() => setUseCustom(!useCustom)}
|
|
>
|
|
{useCustom ? '← Aus Katalog wählen' : 'Freie Eingabe →'}
|
|
</button>
|
|
</div>
|
|
|
|
{useCustom ? (
|
|
<input
|
|
type="text"
|
|
value={item}
|
|
onChange={(e) => setItem(e.target.value)}
|
|
placeholder={getPlaceholder()}
|
|
className={styles.formInput}
|
|
autoFocus
|
|
/>
|
|
) : (
|
|
<select
|
|
value={item}
|
|
onChange={(e) => setItem(e.target.value)}
|
|
className={styles.formSelect}
|
|
>
|
|
<option value="">-- Global (alle Objekte) --</option>
|
|
{Object.entries(groupedObjects).map(([feature, objs]) => (
|
|
<optgroup key={feature} label={feature.toUpperCase()}>
|
|
{objs.map(obj => (
|
|
<option key={obj.objectKey} value={obj.objectKey}>
|
|
{obj.objectKey} - {getLabel(obj)}
|
|
</option>
|
|
))}
|
|
</optgroup>
|
|
))}
|
|
</select>
|
|
)}
|
|
|
|
<span className={styles.formHint}>
|
|
Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).
|
|
</span>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.formLabel}>
|
|
<input
|
|
type="checkbox"
|
|
checked={view}
|
|
onChange={(e) => setView(e.target.checked)}
|
|
style={{ marginRight: '0.5rem' }}
|
|
/>
|
|
Sichtbar (View)
|
|
</label>
|
|
</div>
|
|
|
|
{context === 'DATA' && (
|
|
<div className={styles.addRuleMatrix}>
|
|
{/* Header Row */}
|
|
<div className={styles.matrixHeader}>
|
|
<div className={styles.matrixLabel}></div>
|
|
<div className={styles.matrixGroup}>Eigene (m)</div>
|
|
<div className={styles.matrixGroup}>Gruppe (g)</div>
|
|
<div className={styles.matrixGroup}>Alle (a)</div>
|
|
</div>
|
|
|
|
{/* 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 (
|
|
<div key={op} className={styles.matrixRow}>
|
|
<div className={styles.matrixLabel}>{labels[op]}</div>
|
|
{(['m', 'g', 'a'] as const).map(level => (
|
|
<div key={level} className={styles.matrixCell}>
|
|
<input
|
|
type="checkbox"
|
|
checked={value === level || (level === 'm' && (value === 'g' || value === 'a')) || (level === 'g' && value === 'a')}
|
|
onChange={(e) => {
|
|
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'}`}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.formActions}>
|
|
<button type="button" className={styles.secondaryButton} onClick={onCancel}>
|
|
Abbrechen
|
|
</button>
|
|
<button type="submit" className={styles.primaryButton}>
|
|
<FaPlus /> Hinzufügen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// RULES SECTION
|
|
// =============================================================================
|
|
|
|
interface RulesSectionProps {
|
|
context: RuleContext;
|
|
rules: AccessRule[];
|
|
availableObjects: CatalogObject[];
|
|
readOnly?: boolean;
|
|
onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
|
|
onDelete: (ruleId: string) => void;
|
|
onAdd: (rule: AccessRuleCreate) => void;
|
|
}
|
|
|
|
const RulesSection: React.FC<RulesSectionProps> = ({
|
|
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 <FaTable />;
|
|
case 'UI': return <FaDesktop />;
|
|
case 'RESOURCE': return <FaServer />;
|
|
}
|
|
};
|
|
|
|
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 (
|
|
<div className={styles.rulesSection}>
|
|
{!readOnly && !showAddForm && (
|
|
<div className={styles.sectionHeader}>
|
|
<span className={styles.sectionTitle}>
|
|
{rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'}
|
|
</span>
|
|
<div className={styles.headerActions}>
|
|
{/* View Toggle */}
|
|
{context === 'DATA' && rules.length > 0 && (
|
|
<>
|
|
<button
|
|
className={`${styles.viewToggleButton} ${useTableView ? styles.active : ''}`}
|
|
onClick={() => setUseTableView(true)}
|
|
title="Tabellenansicht"
|
|
>
|
|
<FaThList />
|
|
</button>
|
|
<button
|
|
className={`${styles.viewToggleButton} ${!useTableView ? styles.active : ''}`}
|
|
onClick={() => setUseTableView(false)}
|
|
title="Kartenansicht"
|
|
>
|
|
<FaTh />
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
className={styles.addButton}
|
|
onClick={() => setShowAddForm(true)}
|
|
>
|
|
<FaPlus /> Neue Regel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showAddForm && (
|
|
<AddRuleForm
|
|
context={context}
|
|
availableObjects={availableObjects}
|
|
onAdd={handleAdd}
|
|
onCancel={() => setShowAddForm(false)}
|
|
/>
|
|
)}
|
|
|
|
{rules.length === 0 && !showAddForm ? (
|
|
<div className={styles.emptyState}>
|
|
<div className={styles.emptyIcon}>{getEmptyIcon()}</div>
|
|
<p className={styles.emptyText}>{getEmptyText()}</p>
|
|
{!readOnly && (
|
|
<p className={styles.emptyHint}>
|
|
Klicken Sie auf "Neue Regel" um eine Berechtigung hinzuzufügen.
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : useTableView && context === 'DATA' ? (
|
|
<AccessRulesTable
|
|
rules={rules}
|
|
context={context}
|
|
readOnly={readOnly}
|
|
onUpdate={onUpdate}
|
|
onDelete={onDelete}
|
|
/>
|
|
) : (
|
|
rules.map(rule => (
|
|
<RuleCard
|
|
key={rule.id}
|
|
rule={rule}
|
|
readOnly={readOnly}
|
|
onUpdate={onUpdate}
|
|
onDelete={onDelete}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// JSON EDITOR
|
|
// =============================================================================
|
|
|
|
interface JsonEditorProps {
|
|
rules: AccessRule[];
|
|
readOnly?: boolean;
|
|
onApply: (rules: AccessRule[]) => void;
|
|
}
|
|
|
|
const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) => {
|
|
const [jsonText, setJsonText] = useState('');
|
|
const [error, setError] = useState<string | null>(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 (
|
|
<div className={styles.jsonEditor}>
|
|
<textarea
|
|
value={jsonText}
|
|
onChange={(e) => setJsonText(e.target.value)}
|
|
className={styles.jsonTextarea}
|
|
readOnly={readOnly}
|
|
spellCheck={false}
|
|
/>
|
|
{error && <div className={styles.jsonError}>{error}</div>}
|
|
<p className={styles.jsonHint}>
|
|
Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON.
|
|
Änderungen werden erst nach Klick auf "Anwenden" übernommen.
|
|
</p>
|
|
{!readOnly && (
|
|
<div className={styles.formActions}>
|
|
<button
|
|
type="button"
|
|
className={styles.primaryButton}
|
|
onClick={handleApply}
|
|
disabled={!!error}
|
|
>
|
|
JSON anwenden
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
|
roleId,
|
|
roleName,
|
|
isTemplate = false,
|
|
readOnly = false,
|
|
onSave,
|
|
apiBasePath = '/api/rbac',
|
|
mandateId,
|
|
featureCode,
|
|
}) => {
|
|
const {
|
|
rules,
|
|
loading,
|
|
saving,
|
|
error,
|
|
fetchRules,
|
|
saveRules,
|
|
getGroupedRules,
|
|
updateRuleLocally,
|
|
addRuleLocally,
|
|
removeRuleLocally,
|
|
} = useAccessRules(roleId, apiBasePath, mandateId);
|
|
|
|
// Catalog objects for dropdown selection
|
|
const { objects: catalogObjects, fetchObjects } = useCatalogObjects();
|
|
|
|
const [activeTab, setActiveTab] = useState<TabType>('DATA');
|
|
const [hasChanges, setHasChanges] = useState(false);
|
|
const [originalRules, setOriginalRules] = useState<AccessRule[]>([]);
|
|
|
|
// Load rules on mount
|
|
useEffect(() => {
|
|
fetchRules().then(fetchedRules => {
|
|
setOriginalRules(fetchedRules);
|
|
});
|
|
}, [fetchRules]);
|
|
|
|
// Load catalog objects - filter by featureCode if provided
|
|
useEffect(() => {
|
|
fetchObjects(undefined, featureCode, mandateId);
|
|
}, [fetchObjects, featureCode, mandateId]);
|
|
|
|
// Get objects for current tab
|
|
const currentContextObjects = useMemo(() => {
|
|
if (activeTab === 'JSON') return [];
|
|
return catalogObjects[activeTab] || [];
|
|
}, [catalogObjects, activeTab]);
|
|
|
|
// Track changes
|
|
useEffect(() => {
|
|
setHasChanges(JSON.stringify(rules) !== JSON.stringify(originalRules));
|
|
}, [rules, originalRules]);
|
|
|
|
const groupedRules = getGroupedRules();
|
|
|
|
// Handlers
|
|
const handleUpdate = useCallback((ruleId: string, updates: Partial<AccessRule>) => {
|
|
updateRuleLocally(ruleId, updates);
|
|
}, [updateRuleLocally]);
|
|
|
|
const handleDelete = useCallback((ruleId: string) => {
|
|
if (window.confirm('Möchten Sie diese Regel wirklich löschen?')) {
|
|
removeRuleLocally(ruleId);
|
|
}
|
|
}, [removeRuleLocally]);
|
|
|
|
const handleAdd = useCallback((ruleData: AccessRuleCreate) => {
|
|
const newRule: AccessRule = {
|
|
id: `temp-${Date.now()}`, // Temporary ID
|
|
roleId,
|
|
context: ruleData.context,
|
|
item: ruleData.item || null,
|
|
view: ruleData.view ?? true,
|
|
read: ruleData.read ?? null,
|
|
create: ruleData.create ?? null,
|
|
update: ruleData.update ?? null,
|
|
delete: ruleData.delete ?? null,
|
|
};
|
|
addRuleLocally(newRule);
|
|
}, [roleId, addRuleLocally]);
|
|
|
|
const handleSave = async () => {
|
|
const result = await saveRules(rules);
|
|
if (result.success) {
|
|
setOriginalRules(rules);
|
|
setHasChanges(false);
|
|
onSave?.();
|
|
} else {
|
|
alert(result.error || 'Fehler beim Speichern');
|
|
}
|
|
};
|
|
|
|
const handleReset = () => {
|
|
if (window.confirm('Alle Änderungen verwerfen?')) {
|
|
fetchRules().then(fetchedRules => {
|
|
setOriginalRules(fetchedRules);
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleJsonApply = (newRules: AccessRule[]) => {
|
|
// Replace all rules
|
|
newRules.forEach((rule, index) => {
|
|
if (!rule.id) {
|
|
rule.id = `temp-${Date.now()}-${index}`;
|
|
}
|
|
rule.roleId = roleId;
|
|
});
|
|
// This is a bit hacky - we need to update the store
|
|
// For now, we'll just save directly
|
|
saveRules(newRules);
|
|
};
|
|
|
|
// Render tabs
|
|
const tabs: { id: TabType; label: string; icon: React.ReactNode; count: number }[] = [
|
|
{ id: 'DATA', label: 'Daten', icon: <FaTable />, count: groupedRules.DATA.length },
|
|
{ id: 'UI', label: 'UI', icon: <FaDesktop />, count: groupedRules.UI.length },
|
|
{ id: 'RESOURCE', label: 'Ressourcen', icon: <FaServer />, count: groupedRules.RESOURCE.length },
|
|
{ id: 'JSON', label: 'JSON', icon: <FaCode />, count: rules.length },
|
|
];
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className={styles.accessRulesEditor}>
|
|
<div className={styles.loadingContainer}>
|
|
<div className={styles.spinner} />
|
|
<span>Lade Berechtigungen...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.accessRulesEditor}>
|
|
<div className={styles.editorHeader}>
|
|
<h3 className={styles.editorTitle}>
|
|
Berechtigungen{roleName ? `: ${roleName}` : ''}
|
|
{isTemplate && <span className={styles.templateBadge}>Template</span>}
|
|
</h3>
|
|
{!readOnly && hasChanges && (
|
|
<div className={styles.headerActions}>
|
|
<button
|
|
className={styles.secondaryButton}
|
|
onClick={handleReset}
|
|
disabled={saving}
|
|
>
|
|
<FaUndo /> Zurücksetzen
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className={styles.jsonError}>
|
|
Fehler: {error}
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.tabsContainer}>
|
|
<div className={styles.tabList}>
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
className={`${styles.tab} ${activeTab === tab.id ? styles.active : ''}`}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
>
|
|
<span className={styles.tabIcon}>{tab.icon}</span>
|
|
{tab.label}
|
|
<span className={styles.tabBadge}>{tab.count}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className={styles.tabContent}>
|
|
{activeTab === 'DATA' && (
|
|
<RulesSection
|
|
context="DATA"
|
|
rules={groupedRules.DATA}
|
|
availableObjects={catalogObjects.DATA || []}
|
|
readOnly={readOnly}
|
|
onUpdate={handleUpdate}
|
|
onDelete={handleDelete}
|
|
onAdd={handleAdd}
|
|
/>
|
|
)}
|
|
{activeTab === 'UI' && (
|
|
<RulesSection
|
|
context="UI"
|
|
rules={groupedRules.UI}
|
|
availableObjects={catalogObjects.UI || []}
|
|
readOnly={readOnly}
|
|
onUpdate={handleUpdate}
|
|
onDelete={handleDelete}
|
|
onAdd={handleAdd}
|
|
/>
|
|
)}
|
|
{activeTab === 'RESOURCE' && (
|
|
<RulesSection
|
|
context="RESOURCE"
|
|
rules={groupedRules.RESOURCE}
|
|
availableObjects={catalogObjects.RESOURCE || []}
|
|
readOnly={readOnly}
|
|
onUpdate={handleUpdate}
|
|
onDelete={handleDelete}
|
|
onAdd={handleAdd}
|
|
/>
|
|
)}
|
|
{activeTab === 'JSON' && (
|
|
<JsonEditor
|
|
rules={rules}
|
|
readOnly={readOnly}
|
|
onApply={handleJsonApply}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!readOnly && (
|
|
<div className={styles.actionBar}>
|
|
<button
|
|
className={styles.primaryButton}
|
|
onClick={handleSave}
|
|
disabled={saving || !hasChanges}
|
|
>
|
|
{saving ? (
|
|
<>
|
|
<FaSpinner className="spinning" /> Speichern...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FaSave /> Speichern
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AccessRulesEditor;
|