252 lines
8 KiB
TypeScript
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;
|