ui-nyla/src/components/AccessRules/AccessRulesTable.tsx
2026-04-11 19:44:52 +02:00

252 lines
8 KiB
TypeScript

/**
* AccessRulesTable
*
* Checkbox-based compact table for editing RBAC access rules.
* Shows all permissions in a matrix format similar to Unix permissions.
*/
import React from 'react';
import { FaTable, FaDesktop, FaServer, FaTrash } from 'react-icons/fa';
import { type AccessRule, type RuleContext, type AccessLevel } from '../../hooks/useAccessRules';
import styles from './AccessRules.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
// =============================================================================
// TYPES
// =============================================================================
interface AccessRulesTableProps {
rules: AccessRule[];
context: RuleContext;
readOnly?: boolean;
onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
onDelete: (ruleId: string) => void;
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Check if access level is at least the specified minimum level.
* Hierarchy: n (none) < m (mine) < g (group) < a (all)
*/
const hasLevel = (level: AccessLevel | null | undefined, minLevel: 'm' | 'g' | 'a'): boolean => {
if (!level || level === 'n') return false;
const hierarchy = ['n', 'm', 'g', 'a'];
return hierarchy.indexOf(level) >= hierarchy.indexOf(minLevel);
};
/**
* Calculate the new access level when a checkbox is toggled.
*/
const calculateNewLevel = (
_currentLevel: AccessLevel | null | undefined,
targetLevel: 'm' | 'g' | 'a',
checked: boolean
): AccessLevel => {
if (checked) {
// Activating: set to target level
return targetLevel;
} else {
// Deactivating: set to level below target
const hierarchy: AccessLevel[] = ['n', 'm', 'g', 'a'];
const targetIndex = hierarchy.indexOf(targetLevel);
return hierarchy[targetIndex - 1] || 'n';
}
};
// =============================================================================
// RULE ROW COMPONENT
// =============================================================================
interface AccessRuleRowProps {
rule: AccessRule;
isDataContext: boolean;
readOnly?: boolean;
onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
onDelete: (ruleId: string) => void;
}
const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
rule,
isDataContext,
readOnly,
onUpdate,
onDelete,
}) => {
const { t } = useLanguage();
const opTitle = (op: 'create' | 'read' | 'update' | 'delete') =>
({ create: t('Erstellen'), read: t('Lesen'), update: t('Bearbeiten'), delete: t('Löschen') })[op];
const handleLevelToggle = (
field: 'read' | 'create' | 'update' | 'delete',
targetLevel: 'm' | 'g' | 'a',
checked: boolean
) => {
const currentLevel = rule[field] as AccessLevel | null | undefined;
const newLevel = calculateNewLevel(currentLevel, targetLevel, checked);
onUpdate(rule.id, { [field]: newLevel });
};
// Get icon for context
const getContextIcon = () => {
switch (rule.context) {
case 'DATA': return <FaTable />;
case 'UI': return <FaDesktop />;
case 'RESOURCE': return <FaServer />;
default: return <FaTable />;
}
};
return (
<tr className={styles.ruleRow}>
{/* Object Name */}
<td className={styles.objectCell}>
<span className={styles.objectIcon}>{getContextIcon()}</span>
<code className={styles.objectCode}>{rule.item || '(global)'}</code>
</td>
{/* View Checkbox */}
<td className={styles.checkboxCell}>
<input
type="checkbox"
checked={rule.view}
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
disabled={readOnly}
title={t('Sichtbar')}
/>
</td>
{/* CRUD Checkboxes for DATA context */}
{isDataContext && (
<>
{/* EIGENE (m) */}
{(['create', 'read', 'update', 'delete'] as const).map(op => (
<td key={`m-${op}`} className={styles.checkboxCell}>
<input
type="checkbox"
checked={hasLevel(rule[op] as AccessLevel, 'm')}
onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)}
disabled={readOnly}
title={`${opTitle(op)} - ${t('Eigene')}`}
/>
</td>
))}
{/* GRUPPE (g) */}
{(['create', 'read', 'update', 'delete'] as const).map(op => (
<td key={`g-${op}`} className={styles.checkboxCell}>
<input
type="checkbox"
checked={hasLevel(rule[op] as AccessLevel, 'g')}
onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)}
disabled={readOnly}
title={`${opTitle(op)} - ${t('Gruppe')}`}
/>
</td>
))}
{/* ALLE (a) */}
{(['create', 'read', 'update', 'delete'] as const).map(op => (
<td key={`a-${op}`} className={styles.checkboxCell}>
<input
type="checkbox"
checked={hasLevel(rule[op] as AccessLevel, 'a')}
onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)}
disabled={readOnly}
title={`${opTitle(op)} - ${t('Alle')}`}
/>
</td>
))}
</>
)}
{/* Delete Button */}
<td className={styles.actionsCell}>
{!readOnly && (
<button
className={`${styles.iconButton} ${styles.danger}`}
onClick={() => onDelete(rule.id)}
title={t('Regel löschen')}
>
<FaTrash />
</button>
)}
</td>
</tr>
);
};
// =============================================================================
// MAIN TABLE COMPONENT
// =============================================================================
export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
rules,
context,
readOnly,
onUpdate,
onDelete,
}) => {
const { t } = useLanguage();
const isDataContext = context === 'DATA';
if (rules.length === 0) {
return null;
}
return (
<div className={styles.tableWrapper}>
<table className={styles.accessRulesTable}>
<thead>
<tr>
<th className={styles.colObject}>{t('object dot notation')}</th>
<th className={styles.colView}>{t('Ansicht')}</th>
{isDataContext && (
<>
<th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('group')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('Alle')}</th>
</>
)}
<th className={styles.colActions}></th>
</tr>
{isDataContext && (
<tr className={styles.subHeader}>
<th></th>
<th></th>
<th title={t('Erstellen')}>C</th>
<th title={t('Lesen')}>R</th>
<th title={t('Bearbeiten')}>U</th>
<th title={t('Löschen')}>D</th>
<th title={t('Erstellen')}>C</th>
<th title={t('Lesen')}>R</th>
<th title={t('Bearbeiten')}>U</th>
<th title={t('Löschen')}>D</th>
<th title={t('Erstellen')}>C</th>
<th title={t('Lesen')}>R</th>
<th title={t('Bearbeiten')}>U</th>
<th title={t('Löschen')}>D</th>
<th></th>
</tr>
)}
</thead>
<tbody>
{rules.map(rule => (
<AccessRuleRow
key={rule.id}
rule={rule}
isDataContext={isDataContext}
readOnly={readOnly}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</tbody>
</table>
</div>
);
};
export default AccessRulesTable;